From 0f482d80bcc7a05dbeade67de26bd3a5c10edbbe Mon Sep 17 00:00:00 2001 From: Inso <insomniak.fr@gmail.com> Date: Fri, 21 Aug 2015 09:15:11 +0200 Subject: [PATCH] Handling transaction with the multibranch feature --- src/cutecoin/core/account.py | 4 +- src/cutecoin/core/app.py | 4 +- src/cutecoin/core/config.py | 2 +- src/cutecoin/core/net/network.py | 13 ++-- src/cutecoin/core/net/node.py | 2 +- src/cutecoin/core/transfer.py | 48 +++++-------- src/cutecoin/core/txhistory.py | 103 ++++++++++++++++----------- src/cutecoin/core/wallet.py | 12 ++-- src/cutecoin/gui/community_tab.py | 2 +- src/cutecoin/gui/transactions_tab.py | 4 +- src/cutecoin/models/txhistory.py | 34 ++++++--- 11 files changed, 124 insertions(+), 104 deletions(-) diff --git a/src/cutecoin/core/account.py b/src/cutecoin/core/account.py index a0802e38..1cfa5fb2 100644 --- a/src/cutecoin/core/account.py +++ b/src/cutecoin/core/account.py @@ -230,7 +230,7 @@ class Account(QObject): self.communities.append(community) return community - def refresh_transactions(self, community): + def refresh_transactions(self, app, community): """ Refresh the local account cache This needs n_wallets * n_communities cache refreshing to end @@ -268,7 +268,7 @@ class Account(QObject): for w in self.wallets: w.refresh_progressed.connect(progressing) w.refresh_finished.connect(wallet_finished) - w.init_cache(community) + w.init_cache(app, community) w.refresh_transactions(community, received_list) def set_display_referential(self, index): diff --git a/src/cutecoin/core/app.py b/src/cutecoin/core/app.py index 227459b4..6e8344f1 100644 --- a/src/cutecoin/core/app.py +++ b/src/cutecoin/core/app.py @@ -268,14 +268,14 @@ class Application(QObject): for wallet in account.wallets: for c in account.communities: - wallet.init_cache(c) + wallet.init_cache(self, c) wallet_path = os.path.join(config.parameters['home'], account.name, '__cache__', wallet.pubkey + "_wal") if os.path.exists(wallet_path): with open(wallet_path, 'r') as json_data: data = json.load(json_data) if 'version' in data and data['version'] == __version__: - wallet.load_caches(data) + wallet.load_caches(self, data) else: os.remove(wallet_path) diff --git a/src/cutecoin/core/config.py b/src/cutecoin/core/config.py index 8a4202d2..d05225ba 100644 --- a/src/cutecoin/core/config.py +++ b/src/cutecoin/core/config.py @@ -49,6 +49,6 @@ def parse_arguments(argv): level=logging.INFO) else: logging.getLogger().propagate = False - + logging.getLogger('quamash').setLevel(logging.INFO) logfile = FileHandler(path.join(parameters['home'], 'cutecoin.log')) logging.getLogger().addHandler(logfile) diff --git a/src/cutecoin/core/net/network.py b/src/cutecoin/core/net/network.py index 9f83fb0d..3f959b21 100644 --- a/src/cutecoin/core/net/network.py +++ b/src/cutecoin/core/net/network.py @@ -9,6 +9,7 @@ import logging import time import asyncio from ucoinpy.documents.peer import Peer +from ucoinpy.documents.block import Block from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QTimer @@ -84,6 +85,7 @@ class Network(QObject): node = Node.from_json(network_manager, currency, data) nodes.append(node) network = cls(network_manager, currency, nodes) + # We block the signals until loading the nodes cache return network def jsonify(self): @@ -159,8 +161,11 @@ class Network(QObject): Get the latest block considered valid It is the most frequent last block of every known nodes """ - blocks = [n.block_hash for n in self.nodes] - return max(set(blocks), key=blocks.count) + blocks = [n.block_hash for n in self.nodes if n.block_hash != Block.Empty_Hash] + if len(blocks) > 0: + return max(set(blocks), key=blocks.count) + else: + return Block.Empty_Hash def add_node(self, node): """ @@ -235,8 +240,8 @@ class Network(QObject): self.nodes.remove(node) self.nodes_changed.emit() - logging.debug("{0} -> {1}".format(self.latest_block_number, self.latest_block_number)) - if self._block_found != self.latest_block_hash: + logging.debug("{0} -> {1}".format(self._block_found[:10], self.latest_block_hash[:10])) + if self._block_found != self.latest_block_hash and node.state == Node.ONLINE: logging.debug("Latest block changed : {0}".format(self.latest_block_number)) self._block_found = self.latest_block_hash self.new_block_mined.emit(self.latest_block_number) diff --git a/src/cutecoin/core/net/node.py b/src/cutecoin/core/net/node.py index ff6afaea..58ea1c8d 100644 --- a/src/cutecoin/core/net/node.py +++ b/src/cutecoin/core/net/node.py @@ -115,7 +115,7 @@ class Node(QObject): node = cls(network_manager, peer.currency, [Endpoint.from_inline(e.inline()) for e in peer.endpoints], - "", pubkey, 0, + "", pubkey, 0, Block.Empty_Hash, Node.ONLINE, time.time(), {'root': "", 'leaves': []}, "", "") diff --git a/src/cutecoin/core/transfer.py b/src/cutecoin/core/transfer.py index f7785927..b0807af1 100644 --- a/src/cutecoin/core/transfer.py +++ b/src/cutecoin/core/transfer.py @@ -14,8 +14,8 @@ class Transfer(QObject): """ A transfer is the lifecycle of a transaction. TO_SEND means the transaction wasn't sent yet - AWAITING means the transaction is waiting for a blockchain validation - VALIDATED means the transaction was registered in the blockchain + AWAITING means the transaction is waiting to reach K blockchain validation + VALIDATED means the transaction was validated locally and is considered present in the blockchain REFUSED means the transaction took too long to be registered in the blockchain, therefore it is considered as refused DROPPED means the transaction was canceled locally. It can still be validated @@ -23,6 +23,7 @@ class Transfer(QObject): """ TO_SEND = 0 AWAITING = 1 + VALIDATING = 4 VALIDATED = 2 REFUSED = 3 DROPPED = 5 @@ -68,11 +69,11 @@ class Transfer(QObject): return cls(None, Transfer.TO_SEND, metadata) @classmethod - def create_validated(cls, hash, metadata): + def create_from_blockchain(cls, hash, state, metadata): """ - Create a new transfer in a "VALIDATED" state. + Create a new transfer sent from another cutecoin instance """ - return cls(hash, Transfer.VALIDATED, metadata) + return cls(hash, state, metadata) @classmethod def load(cls, data): @@ -137,7 +138,7 @@ class Transfer(QObject): return self.broadcast_error.emit(r.error(), strdata) - def check_registered(self, tx, block, time): + def check_registered(self, tx, block, time, data_validation): """ Check if the transfer was registered in a block. Update the transfer state to VALIDATED if it was registered. @@ -147,19 +148,23 @@ class Transfer(QObject): :param int time: The time of the block """ if tx.signed_raw() == self.txdoc.signed_raw(): - self.state = Transfer.VALIDATED - self._metadata['block'] = block - self._metadata['time'] = time + if self.state == Transfer.AWAITING: + self.state = Transfer.VALIDATING + self._metadata['block'] = block + self._metadata['time'] = time + elif self.state == Transfer.VALIDATING and \ + self._metadata['block'] - block > data_validation: + self.state = Transfer.VALIDATED - def check_refused(self, block): + def check_refused(self, time, block_time, mediantime_blocks): """ Check if the transfer was refused - If more than 15 blocks were mined since the transaction + If more than block_time*15 seconds passed since transfer, it is considered as refused. :param int block: The current block number """ - if block > self._metadata['block'] + 15: + if time > self._metadata['time'] + block_time*mediantime_blocks*10: self.state = Transfer.REFUSED def drop(self): @@ -169,22 +174,3 @@ class Transfer(QObject): """ self.state = Transfer.DROPPED - -class Received(Transfer): - def __init__(self, hash, metadata): - """ - A transfer were the receiver is the local user. - - :param txdoc: The transaction document of the received transfer - :param metadata: The metadata of the transfer - """ - super().__init__(hash, Transfer.VALIDATED, metadata) - - @classmethod - def load(cls, data): - """ - Create a transfer from a dict in json format. - - :param data: The transfer as a dict in json format - """ - return cls(data['hash'], data['metadata']) diff --git a/src/cutecoin/core/txhistory.py b/src/cutecoin/core/txhistory.py index 2a650988..dfab7c97 100644 --- a/src/cutecoin/core/txhistory.py +++ b/src/cutecoin/core/txhistory.py @@ -1,14 +1,16 @@ import asyncio import logging -from .transfer import Transfer, Received +from .transfer import Transfer from ucoinpy.documents.transaction import InputSource, OutputSource from ..tools.exceptions import LookupFailureError from .net.api import bma as qtbma + class TxHistory(): - def __init__(self, wallet): + def __init__(self, app, wallet): self._latest_block = 0 self.wallet = wallet + self.app = app self._stop_coroutines = False self._transfers = [] @@ -28,10 +30,7 @@ class TxHistory(): data_sent = data['transfers'] for s in data_sent: - if s['metadata']['issuer'] == self.wallet.pubkey: - self._transfers.append(Transfer.load(s)) - else: - self._transfers.append(Received.load(s)) + self._transfers.append(Transfer.load(s)) for s in data['sources']: self.available_sources.append(InputSource.from_inline(s['inline'])) @@ -72,12 +71,17 @@ class TxHistory(): self._stop_coroutines = True @asyncio.coroutine - def _parse_transaction(self, community, txdata, received_list, txid): + def _parse_transaction(self, community, txdata, received_list, txid, current_block): tx_outputs = [OutputSource.from_inline(o) for o in txdata['outputs']] receivers = [o.pubkey for o in tx_outputs if o.pubkey != txdata['issuers'][0]] block_number = txdata['block_number'] + if block_number + self.app.preferences['data_validation'] >= current_block: + state = Transfer.VALIDATED + else: + state = Transfer.VALIDATING + mediantime = txdata['time'] logging.debug(txdata) @@ -109,35 +113,44 @@ class TxHistory(): if i == self.wallet.pubkey]) > 0 in_outputs = len([o for o in tx_outputs if o.pubkey == self.wallet.pubkey]) > 0 - - # If the wallet pubkey is in the issuers we sent this transaction - if in_issuers: - outputs = [o for o in tx_outputs - if o.pubkey != self.wallet.pubkey] - amount = 0 - for o in outputs: - amount += o.amount - metadata['amount'] = amount - - awaiting = [t for t in self._transfers - if t.state == Transfer.AWAITING] - # We check if the transaction correspond to one we sent - if txdata['hash'] not in [t['hash'] for t in awaiting]: - transfer = Transfer.create_validated(txdata['hash'], + awaiting = [t for t in self._transfers + if t.state in (Transfer.AWAITING, Transfer.VALIDATING)] + + # We check if the transaction correspond to one we sent + # but not from this cutecoin Instance + if txdata['hash'] not in [t.metadata['hash'] for t in awaiting]: + # If the wallet pubkey is in the issuers we sent this transaction + if in_issuers: + outputs = [o for o in tx_outputs + if o.pubkey != self.wallet.pubkey] + amount = 0 + for o in outputs: + amount += o.amount + metadata['amount'] = amount + transfer = Transfer.create_from_blockchain(txdata['hash'], + state, metadata.copy()) return transfer - # If we are not in the issuers, - # maybe it we are in the recipients of this transaction - elif in_outputs: - outputs = [o for o in tx_outputs - if o.pubkey == self.wallet.pubkey] - amount = 0 - for o in outputs: - amount += o.amount - metadata['amount'] = amount - received = Received(txdata['hash'], metadata.copy()) - received_list.append(received) - return received + # If we are not in the issuers, + # maybe it we are in the recipients of this transaction + elif in_outputs: + outputs = [o for o in tx_outputs + if o.pubkey == self.wallet.pubkey] + amount = 0 + for o in outputs: + amount += o.amount + metadata['amount'] = amount + + if txdata['hash'] not in [t['hash'] for t in awaiting]: + transfer = Transfer.create_from_blockchain(txdata['hash'], + state, + metadata.copy()) + received_list.append(transfer) + return transfer + else: + transfer = [t for t in awaiting if t.metadata['hash'] == txdata['hash']][0] + transfer.check_registered(txdata['hash'], current_block, mediantime, + self.app.preferences['data_validation']) return None @asyncio.coroutine @@ -148,9 +161,11 @@ class TxHistory(): :param cutecoin.core.Community community: The community :param list received_list: List of transactions received """ - parsed_block = self.latest_block - current_block = community.network.latest_block_number - logging.debug("Refresh from : {0} to {1}".format(self.latest_block, current_block)) + current_block = yield from community.bma_access.future_request(qtbma.blockchain.Block, + req_args={'number': community.network.latest_block_number}) + + parsed_block = min(self.latest_block, current_block['number'] - self.app.preferences['data_validation']) + logging.debug("Refresh from : {0} to {1}".format(self.latest_block, current_block['number'])) dividends_data = qtbma.ud.History.null_value while dividends_data == qtbma.ud.History.null_value: dividends_data = yield from community.bma_access.future_request(qtbma.ud.History, @@ -166,7 +181,7 @@ class TxHistory(): # Lets look if transactions took too long to be validated awaiting = [t for t in self._transfers if t.state == Transfer.AWAITING] - while parsed_block < current_block: + while parsed_block < current_block['number']: tx_history = qtbma.tx.history.Blocks.null_value while tx_history == qtbma.tx.history.Blocks.null_value: tx_history = yield from community.bma_access.future_request(qtbma.tx.history.Blocks, @@ -191,23 +206,25 @@ class TxHistory(): if len(txdata['issuers']) == 0: logging.debug("Error with : {0}, from {1} to {2}".format(self.wallet.pubkey, parsed_block, - current_block)) + current_block['number'])) else: transfer = yield from self._parse_transaction(community, txdata, received_list, udid + txid) if transfer: new_transfers.append(transfer) - self.wallet.refresh_progressed.emit(parsed_block, current_block, self.wallet.pubkey) + self.wallet.refresh_progressed.emit(parsed_block, current_block['number'], self.wallet.pubkey) parsed_block += 100 - if current_block > self.latest_block: + if current_block['number'] > self.latest_block: self.available_sources = yield from self.wallet.future_sources(community) if self._stop_coroutines: return - self.latest_block = current_block + self.latest_block = current_block['number'] for transfer in awaiting: - transfer.check_refused(current_block) + transfer.check_refused(current_block['medianTime'], + community.parameters['avgGenTime'], + community.parameters['medianTimeBlocks']) self._transfers = self._transfers + new_transfers self._dividends = self._dividends + new_dividends diff --git a/src/cutecoin/core/wallet.py b/src/cutecoin/core/wallet.py index 25cdba27..f7ea800b 100644 --- a/src/cutecoin/core/wallet.py +++ b/src/cutecoin/core/wallet.py @@ -10,7 +10,7 @@ from ucoinpy.key import SigningKey from .net.api import bma as qtbma from .net.api.bma import PROTOCOL_VERSION from ..tools.exceptions import NotEnoughMoneyError, NoPeerAvailable, LookupFailureError -from .transfer import Transfer, Received +from .transfer import Transfer from .txhistory import TxHistory from .registry import IdentitiesRegistry, Identity @@ -75,7 +75,7 @@ class Wallet(QObject): name = json_data['name'] return cls(walletid, pubkey, name, identities_registry) - def load_caches(self, json_data): + def load_caches(self, app, json_data): """ Load this wallet caches. Each cache correspond to one different community. @@ -84,7 +84,7 @@ class Wallet(QObject): """ for currency in json_data: if currency != 'version': - self.caches[currency] = TxHistory(self) + self.caches[currency] = TxHistory(app, self) self.caches[currency].load_from_json(json_data[currency]) def jsonify_caches(self): @@ -98,14 +98,14 @@ class Wallet(QObject): data[currency] = self.caches[currency].jsonify() return data - def init_cache(self, community): + def init_cache(self, app, community): """ Init the cache of this wallet for the specified community. :param community: The community to refresh its cache """ if community.currency not in self.caches: - self.caches[community.currency] = TxHistory(self) + self.caches[community.currency] = TxHistory(app, self) def refresh_transactions(self, community, received_list): """ @@ -258,7 +258,7 @@ class Wallet(QObject): except LookupFailureError as e: receiver_uid = "" - metadata = {'block': block_number, + metadata = {'block': None, 'time': time, 'amount': amount, 'issuer': key.pubkey, diff --git a/src/cutecoin/gui/community_tab.py b/src/cutecoin/gui/community_tab.py index 6928f1e8..491c7518 100644 --- a/src/cutecoin/gui/community_tab.py +++ b/src/cutecoin/gui/community_tab.py @@ -145,7 +145,7 @@ class CommunityTabWidget(QWidget, Ui_CommunityTabWidget): self.certify_identity(person) def identity_informations(self, person): - dialog = MemberDialog(none, self.account, self.community, person) + dialog = MemberDialog(None, self.account, self.community, person) dialog.exec_() def add_identity_as_contact(self, person): diff --git a/src/cutecoin/gui/transactions_tab.py b/src/cutecoin/gui/transactions_tab.py index ffc7c825..be4b11b4 100644 --- a/src/cutecoin/gui/transactions_tab.py +++ b/src/cutecoin/gui/transactions_tab.py @@ -80,7 +80,7 @@ class TransactionsTabWidget(QWidget, Ui_transactionsTabWidget): self.progressbar.setMaximum(maximum) self.app.current_account.loading_progressed.connect(progressing) self.app.current_account.loading_finished.connect(self.stop_progress) - self.app.current_account.refresh_transactions(self.community) + self.app.current_account.refresh_transactions(self.app, self.community) self.progressbar.show() @pyqtSlot(list) @@ -202,7 +202,7 @@ class TransactionsTabWidget(QWidget, Ui_transactionsTabWidget): def send_again(self): transfer = self.sender().data() - dialog = TransferMoneyDialog(self.app.current_account, + dialog = TransferMoneyDialog(self.app, self.app.current_account, self.password_asker) dialog.accepted.connect(self.currency_tab.refresh_wallets) sender = transfer.metadata['issuer'] diff --git a/src/cutecoin/models/txhistory.py b/src/cutecoin/models/txhistory.py index 845f1c27..6b07f369 100644 --- a/src/cutecoin/models/txhistory.py +++ b/src/cutecoin/models/txhistory.py @@ -6,7 +6,7 @@ Created on 5 févr. 2014 import datetime import logging -from ..core.transfer import Transfer, Received +from ..core.transfer import Transfer from PyQt5.QtCore import QAbstractTableModel, Qt, QVariant, QSortFilterProxyModel, \ QDateTime, QLocale, QModelIndex @@ -66,7 +66,7 @@ class TxFilterProxyModel(QSortFilterProxyModel): return in_period(date) def columnCount(self, parent): - return self.sourceModel().columnCount(None) - 3 + return self.sourceModel().columnCount(None) - 4 def setSourceModel(self, sourceModel): self.community = sourceModel.community @@ -147,6 +147,13 @@ class TxFilterProxyModel(QSortFilterProxyModel): return Qt.AlignCenter if role == Qt.ToolTipRole: + if state_data == Transfer.VALIDATING: + block_col = model.columns_types.index('block_number') + block_index = model.index(source_index.row(), block_col) + block_data = model.data(block_index, Qt.DisplayRole) + return "{0} / {1} validations".format(self.community.network.latest_block_number - block_data, + self.app.preferences['data_validation']) + if source_index.column() == self.sourceModel().columns_types.index('date'): return QDateTime.fromTime_t(source_data).toString(Qt.SystemLocaleLongDate) @@ -177,7 +184,8 @@ class HistoryTableModel(QAbstractTableModel): 'comment', 'state', 'txid', - 'pubkey' + 'pubkey', + 'block_number' ) self.column_headers = ( @@ -187,8 +195,9 @@ class HistoryTableModel(QAbstractTableModel): self.tr('Deposit'), self.tr('Comment'), 'State', - 'TXID' - 'Pubkey' + 'TXID', + 'Pubkey', + 'Block Number' ) @property @@ -211,10 +220,11 @@ class HistoryTableModel(QAbstractTableModel): date_ts = transfer.metadata['time'] txid = transfer.metadata['txid'] + block_number = transfer.metadata['block'] return (date_ts, sender, "", amount, comment, transfer.state, txid, - transfer.metadata['issuer']) + transfer.metadata['issuer'], block_number) def data_sent(self, transfer): amount = transfer.metadata['amount'] @@ -228,10 +238,11 @@ class HistoryTableModel(QAbstractTableModel): date_ts = transfer.metadata['time'] txid = transfer.metadata['txid'] + block_number = transfer.metadata['block'] return (date_ts, receiver, amount, "", comment, transfer.state, txid, - transfer.metadata['receiver']) + transfer.metadata['receiver'], block_number) def data_dividend(self, dividend): amount = dividend['amount'] @@ -247,10 +258,11 @@ class HistoryTableModel(QAbstractTableModel): self.beginResetModel() self.transfers_data = [] for transfer in self.transfers: - if type(transfer) is Received: - self.transfers_data.append(self.data_received(transfer)) - elif type(transfer) is Transfer: - self.transfers_data.append(self.data_sent(transfer)) + if type(transfer) is Transfer: + if transfer.metadata['issuer'] == self.account.pubkey: + self.transfers_data.append(self.data_sent(transfer)) + else: + self.transfers_data.append(self.data_received(transfer)) elif type(transfer) is dict: self.transfers_data.append(self.data_dividend(transfer)) self.endResetModel() -- GitLab