diff --git a/doc/uml/tx_lifecycle.png b/doc/uml/tx_lifecycle.png index d73d828a1511f68a30f4d03bfdd317e0189c0636..e27439cdf29b9dc51156869612b86feef74afc49 100644 Binary files a/doc/uml/tx_lifecycle.png and b/doc/uml/tx_lifecycle.png differ diff --git a/doc/uml/tx_lifecycle.pu b/doc/uml/tx_lifecycle.pu index 28405b6461c5fdf3dce93e6da1f807144f739411..bce551415a952a9de828c5c9264c0e320c707112 100644 --- a/doc/uml/tx_lifecycle.pu +++ b/doc/uml/tx_lifecycle.pu @@ -1 +1 @@ -@startuml note "With B a Block\nWith W the Median fork window\nWith Cur the current block of the main branch" as N1 state Local_Tx { [*] --> To_send : Signed locally To_send : B = none To_send --> Awaiting : Node answered\n200 OK to POST Awaiting : B = Cur Awaiting --> Refused : Not registered in [B; B+W] Refused --> To_send : Send back Refused --> [*] : Drop } state Registered { [*] --> Validating : Posted\nsin the blockchain Validating : B = Block containing the Tx Awaiting --> Validating : Found in the blockchain Validating --> Validated : Cur-B > W Validated --> Validating : Blockchain\nrollback Validated --> Awaiting : Blockchain\nrollback\ntx local removed Validated --> [*] : Blockchain\nrollback\ntx removed Validating --> [*] : Blockchain\nrollback\ntx removed } @enduml \ No newline at end of file +@startuml note "With B a Block\nWith W the Median fork window\nWith Cur the current block of the main branch\nWith T a time" as N1 state Local_Tx { [*] --> To_send : Signed locally To_send : B = none To_send --> Awaiting : Node answered\n200 OK to POST Awaiting : Time = Cur.MedianTime Awaiting --> Refused : Not registered in [T; T+W*MedianTime] Refused --> To_send : Send back Refused --> [*] : Drop } state Registered { [*] --> Validating : Posted\nsin the blockchain Validating : B = Block containing the Tx Awaiting --> Validating : Found in the blockchain Validating --> Validated : Cur-B > W Validated --> Validating : Blockchain\nrollback Validated --> Awaiting : Blockchain\nrollback\ntx local removed Validated --> [*] : Blockchain\nrollback\ntx removed Validating --> [*] : Blockchain\nrollback\ntx removed } @enduml \ No newline at end of file diff --git a/lib/ucoinpy/documents/__init__.py b/lib/ucoinpy/documents/__init__.py index 9763c366155f445dd55be9022f8d2cacf381036a..2291e35a028452620bc12c2101ee2fd5a4bb8220 100644 --- a/lib/ucoinpy/documents/__init__.py +++ b/lib/ucoinpy/documents/__init__.py @@ -1,44 +1,7 @@ -""" -Created on 3 déc. 2014 - -@author: inso -""" -import base58 -import base64 -import re -import logging -from ..key import Base58Encoder - -class Document: - re_version = re.compile("Version: ([0-9]+)\n") - re_currency = re.compile("Currency: ([^\n]+)\n") - re_signature = re.compile("([A-Za-z0-9+/]+(?:=|==)?)\n") - - def __init__(self, version, currency, signatures): - self.version = version - self.currency = currency - if signatures: - self.signatures = [s for s in signatures if s is not None] - else: - self.signatures = [] - - def sign(self, keys): - """ - Sign the current document. - Warning : current signatures will be replaced with the new ones. - """ - self.signatures = [] - for key in keys: - signing = base64.b64encode(key.signature(bytes(self.raw(), 'ascii'))) - logging.debug("Signature : \n{0}".format(signing.decode("ascii"))) - self.signatures.append(signing.decode("ascii")) - - def signed_raw(self): - """ - If keys are None, returns the raw + current signatures - If keys are present, returns the raw signed by these keys - """ - raw = self.raw() - signed = "\n".join(self.signatures) - signed_raw = raw + signed + "\n" - return signed_raw +from .block import Block, BlockId +from .certification import SelfCertification, Certification +from .membership import Membership +from .peer import Endpoint, BMAEndpoint, UnknownEndpoint, Peer +from .status import Status +from .transaction import SimpleTransaction, Transaction +from .document import Document \ No newline at end of file diff --git a/lib/ucoinpy/documents/block.py b/lib/ucoinpy/documents/block.py index 00ce0f779ce1fc9cef56a5f5b41ecabedf6bd2bd..81fafccca05300252fe4c2edeb54d7a5fca262e7 100644 --- a/lib/ucoinpy/documents/block.py +++ b/lib/ucoinpy/documents/block.py @@ -5,7 +5,7 @@ Created on 2 déc. 2014 """ from .. import PROTOCOL_VERSION -from . import Document +from .document import Document from .certification import SelfCertification, Certification from .membership import Membership from .transaction import Transaction @@ -14,6 +14,36 @@ import re import logging +class BlockId: + """ + A simple block id + """ + re_hash = re.compile("([0-9a-fA-F]{5,40})") + + @classmethod + def empty(cls): + return cls(0, Block.Empty_Hash) + + def __init__(self, number, sha_hash): + assert(type(number) is int) + assert(BlockId.re_hash.match(sha_hash) is not None) + self.number = number + self.sha_hash = sha_hash + + @classmethod + def from_str(cls, blockid): + """ + :param str blockid: The block id + """ + data = blockid.split("-") + number = int(data[0]) + sha_hash = data[1] + return cls(number, sha_hash) + + def __str__(self): + return "{0}-{1}".format(self.number, self.sha_hash) + + class Block(Document): """ Version: VERSION @@ -107,6 +137,10 @@ BOTTOM_SIGNATURE self.certifications = certifications self.transactions = transactions + @property + def blockid(self): + return BlockId(self.number, self.sha_hash) + @classmethod def from_signed_raw(cls, raw): lines = raw.splitlines(True) diff --git a/lib/ucoinpy/documents/certification.py b/lib/ucoinpy/documents/certification.py index d7e79adb550dcc1ce15c10dc20fa7e0629c40758..61999ccf4ca13f7baabe204e886ace19a917afa7 100644 --- a/lib/ucoinpy/documents/certification.py +++ b/lib/ucoinpy/documents/certification.py @@ -7,7 +7,7 @@ import re import base64 import logging -from . import Document +from .document import Document class SelfCertification(Document): diff --git a/lib/ucoinpy/documents/membership.py b/lib/ucoinpy/documents/membership.py index 570110e29772f1ed473f02a5c788cc4505cbe2ca..097d36762c51b94d289b8123258991570baa248a 100644 --- a/lib/ucoinpy/documents/membership.py +++ b/lib/ucoinpy/documents/membership.py @@ -4,7 +4,7 @@ Created on 2 déc. 2014 @author: inso """ from .. import PROTOCOL_VERSION -from . import Document +from .document import Document import re diff --git a/lib/ucoinpy/documents/peer.py b/lib/ucoinpy/documents/peer.py index 6ca7606c6429b7521318720b0336649676a70dd6..80dddef7c73e65c6cc4893f357fd92697aa3f469 100644 --- a/lib/ucoinpy/documents/peer.py +++ b/lib/ucoinpy/documents/peer.py @@ -7,7 +7,7 @@ Created on 2 déc. 2014 import re from ..api.bma import ConnectionHandler -from . import Document +from .document import Document from .. import PROTOCOL_VERSION, MANAGED_API diff --git a/lib/ucoinpy/documents/status.py b/lib/ucoinpy/documents/status.py index f11e7542c338f57831f6972f11a260a10d0d0254..acfd5eb47654b41813a847eb36662fd6f75dae25 100644 --- a/lib/ucoinpy/documents/status.py +++ b/lib/ucoinpy/documents/status.py @@ -5,7 +5,7 @@ Created on 2 déc. 2014 """ import re -from . import Document +from .document import Document class Status(Document): diff --git a/lib/ucoinpy/documents/transaction.py b/lib/ucoinpy/documents/transaction.py index 0a9d40d21b797cd1dfd7f35bcaac230c73170c6a..5f2f4e1333f64eb4ee3844d5012a0fcc092fe97a 100644 --- a/lib/ucoinpy/documents/transaction.py +++ b/lib/ucoinpy/documents/transaction.py @@ -4,10 +4,11 @@ Created on 2 déc. 2014 @author: inso """ -from . import Document +from .document import Document import re import logging + class Transaction(Document): """ Document format : diff --git a/src/cutecoin/core/community.py b/src/cutecoin/core/community.py index d2658542a6e854c294c4d18e673b08fdbf3c2fab..8098594c2005db64d6df90a06983eae9c2d4e589 100644 --- a/src/cutecoin/core/community.py +++ b/src/cutecoin/core/community.py @@ -16,7 +16,7 @@ from PyQt5.QtCore import QObject, pyqtSignal from ..tools.exceptions import NoPeerAvailable from .net.network import Network from ucoinpy.api import bma -from ucoinpy.documents.block import Block +from ucoinpy.documents import Block, BlockId from .net.api.bma.access import BmaAccess @@ -287,14 +287,11 @@ class Community(QObject): try: block = yield from self.bma_access.future_request(bma.blockchain.Current) signed_raw = "{0}{1}\n".format(block['raw'], block['signature']) - block_hash = hashlib.sha1(signed_raw.encode("ascii")).hexdigest().upper() - block_number = block['number'] except ValueError as e: if '404' in str(e): - block_hash = Block.Empty_Hash - block_number = 0 + return BlockId.empty() - return {'number': block_number, 'hash': block_hash} + return Block.from_signed_raw(signed_raw).blockid @asyncio.coroutine def members_pubkeys(self): diff --git a/src/cutecoin/core/net/network.py b/src/cutecoin/core/net/network.py index 4146da8d9b5390f16f3c7b0f3aa818b7647dd45b..1d6030ff3f6b31a7180a40ab2cc3bb519de58270 100644 --- a/src/cutecoin/core/net/network.py +++ b/src/cutecoin/core/net/network.py @@ -12,8 +12,6 @@ import asyncio from ucoinpy.documents.peer import Peer from ucoinpy.documents.block import Block -from ...tools.decorators import asyncify - from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QTimer from collections import Counter diff --git a/src/cutecoin/core/net/node.py b/src/cutecoin/core/net/node.py index a64b6fa879f24689a2971b4a1b28e322a8da4a7a..19c71e9672fef6e69dfc69f6dc2dcc2de5d00bf5 100644 --- a/src/cutecoin/core/net/node.py +++ b/src/cutecoin/core/net/node.py @@ -296,8 +296,8 @@ class Node(QObject): if not self.block or block_hash != self.block['hash']: try: - #TODO: Check previous block - self.main_chain_previous_block = yield from bma.blockchain.Block(conn_handler, + if self.block: + self.main_chain_previous_block = yield from bma.blockchain.Block(conn_handler, self.block['number']).get() except ValueError as e: if '404' in str(e): diff --git a/src/cutecoin/core/transfer.py b/src/cutecoin/core/transfer.py index 29902ca573b85fda70195b39ee820347d87a902a..5a78b7c2a5d5be8f113ce730bafff5cdf410708d 100644 --- a/src/cutecoin/core/transfer.py +++ b/src/cutecoin/core/transfer.py @@ -6,14 +6,14 @@ Created on 31 janv. 2015 import logging import asyncio from ucoinpy.api import bma -from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject -from PyQt5.QtNetwork import QNetworkReply +from ucoinpy.documents import Block, BlockId +from PyQt5.QtCore import pyqtSignal, QObject import hashlib +from enum import Enum -class Transfer(QObject): +class TransferState(Enum): """ - A transfer is the lifecycle of a transaction. TO_SEND means the transaction wasn't sent yet 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 @@ -29,10 +29,15 @@ class Transfer(QObject): REFUSED = 3 DROPPED = 5 + +class Transfer(QObject): + """ + A transfer is the lifecycle of a transaction. + """ transfer_broadcasted = pyqtSignal(str) broadcast_error = pyqtSignal(int, str) - def __init__(self, hash, state, metadata): + def __init__(self, sha_hash, state, blockid, metadata, locally_created): """ The constructor of a transfer. Check for metadata keys which must be present : @@ -43,12 +48,12 @@ class Transfer(QObject): - amount - comment - :param txdoc: The Transaction ucoinpy object - :param state: The state of the Transfer (TO_SEND, AWAITING, VALIDATED, REFUSED or DROPPED) - :param metadata: The transfer metadata + :param str sha_hash: The hash of the transaction + :param TransferState state: The state of the Transfer + :param ucoinpy.documents.BlockId blockid: The blockid of the transaction in the blockchain + :param dict metadata: The transfer metadata """ assert('receiver' in metadata) - assert('block' in metadata) assert('time' in metadata) assert('issuer' in metadata) assert('amount' in metadata) @@ -58,32 +63,81 @@ class Transfer(QObject): assert('txid' in metadata) super().__init__() - self.hash = hash + self.sha_hash = sha_hash self.state = state + self.blockid = blockid + self._locally_created = locally_created self._metadata = metadata + self._table_states = { + (TransferState.TO_SEND, (list, Block)): + (self._broadcast_success, self._wait, TransferState.AWAITING), + + (TransferState.AWAITING, (bool, Block)): + (self._found_in_block, self._be_validating, TransferState.VALIDATING), + (TransferState.AWAITING, (bool, Block, int)): + (self._not_found_in_blockchain, None, TransferState.REFUSED), + + (TransferState.VALIDATING, (bool, Block, int)): + (self._reached_enough_validation, None, TransferState.VALIDATED), + (TransferState.VALIDATING, (bool, Block)): + (self._rollback_and_removed, self._drop, TransferState.DROPPED), + + (TransferState.VALIDATED, (bool, Block)): + (self._rollback_still_present, self._be_validating, TransferState.VALIDATING), + (TransferState.VALIDATED, (bool, Block)): + (self._rollback_and_removed, self._drop, TransferState.DROPPED), + (TransferState.VALIDATED, (bool, Block)): + (self._rollback_and_local, self._wait, TransferState.AWAITING), + + (TransferState.DROPPED, ()): + (self._is_locally_created, None, TransferState.TO_SEND), + (TransferState.REFUSED, ()): + (self._is_locally_created, self._drop, TransferState.DROPPED), + } + @classmethod def initiate(cls, metadata): """ Create a new transfer in a "TO_SEND" state. + :param dict metadata: The computed metadata of the transfer + :return: A new transfer + :rtype: Transfer """ - return cls(None, Transfer.TO_SEND, metadata) + return cls(None, TransferState.TO_SEND, None, metadata, True) @classmethod - def create_from_blockchain(cls, hash, metadata, block_number, time, nb_validation): + def create_from_blockchain(cls, hash, blockid, metadata): """ Create a new transfer sent from another cutecoin instance + :param str hash: The transaction hash + :param ucoinpy.documents.BlockId blockid: The block id were we found the tx + :param dict metadata: The computed metadata of the transaction + :return: A new transfer + :rtype: Transfer """ - tx = cls(hash, Transfer.VALIDATING, metadata) - tx.check_registered(block_number, time, nb_validation) - return tx + return cls(hash, TransferState.VALIDATING, blockid, metadata, False) @classmethod def load(cls, data): """ Create a new transfer from a dict in json format. + :param dict data: The loaded data + :return: A new transfer + :rtype: Transfer """ - return cls(data['hash'], data['state'], data['metadata']) + return cls(data['hash'], TransferState[data['state']], BlockId.from_str(data['blockid']), + data['metadata'], data['local']) + + def jsonify(self): + """ + :return: The transfer as a dict in json format + """ + return {'hash': self.sha_hash, + 'state': self.state.name, + 'blockid': str(self.blockid), + 'metadata': self._metadata, + 'local': self._locally_created} @property def metadata(self): @@ -92,13 +146,159 @@ class Transfer(QObject): """ return self._metadata - def jsonify(self): + def _not_found_in_blockchain(self, rollback, block, mediantime_target): """ - :return: The transfer as a dict in json format + Check if the transaction could not be found in the blockchain + :param bool rollback: True if we are in a rollback procedure + :param ucoinpy.documents.Block block: The block to look for the tx + :param int mediantime_target: The mediantime to mine a block in the community parameters + :return: True if the transaction could not be found in a given time + :rtype: bool + """ + if not rollback: + for tx in block.transactions: + if tx.hash == self.sha_hash: + return False + if block.time > self.metadata['time'] + mediantime_target*10: + return True + return False + + def _found_in_block(self, rollback, block): + """ + Check if the transaction can be found in the blockchain + :param bool rollback: True if we are in a rollback procedure + :param ucoinpy.documents.Block block: The block to check for the transaction + :return: True if the transaction was found + :rtype: bool + """ + if not rollback: + for tx in block.transactions: + if tx.hash == self.sha_hash: + return True + return False + + def _broadcast_success(self, ret_codes, time): + """ + Check if the retcode is 200 after a POST + :param list ret_codes: The POST return codes of the broadcast + :param int time: The mediantime of the blockchain. Used for transition. + :return: True if the post was successful + :rtype: bool + """ + return 200 in ret_codes + + def _reached_enough_validation(self, rollback, current_block, fork_window): """ - return {'hash': self.hash, - 'state': self.state, - 'metadata': self._metadata} + Check if the transfer reached enough validation in the blockchain + :param bool rollback: True if we are in a rollback procedure + :param ucoinpy.documents.Block current_block: The current block of the main blockchain + :param int fork_window: The number of validations needed on the network + :return: True if the transfer reached enough validations + :rtype: bool + """ + return not rollback and self.blockid.number + fork_window <= current_block.number + + def _rollback_and_removed(self, rollback, block): + """ + Check if the transfer is not in the block anymore + :param bool rollback: True if we are in a rollback procedure + :param ucoinpy.documents.Block block: The block to check for the transaction + :return: True if the transfer is not found in the block + """ + if rollback and block.blockid == self.blockid: + return self.sha_hash not in [t.hash for t in block.transactions] + return False + + def _rollback_still_present(self, rollback, block): + """ + Check if the transfer is not in the block anymore + :param bool rollback: True if we are in a rollback procedure + :param ucoinpy.documents.Block block: The block to check for the transaction + :return: True if the transfer is found in the block + """ + if rollback and block.blockid == self.blockid: + return self.sha_hash in [t.hash for t in block.transactions] + return False + + def _rollback_and_local(self, rollback, block): + """ + Check if the transfer is not in the block anymore + :param bool rollback: True if we are in a rollback procedure + :param ucoinpy.documents.Block block: The block to check for the transaction + :return: True if the transfer is found in the block + """ + if rollback and self._locally_created and block.blockid == self.blockid: + return self.sha_hash not in [t.hash for t in block.transactions] + return False + + def _is_locally_created(self): + """ + Check if we can send back the transaction if it was locally created + :return: True if the transaction was locally created + """ + return self._locally_created + + def _wait(self, ret_codes, current_block): + """ + Set the transfer as AWAITING validation. + :param list ret_codes: The responses return codes + :param ucoinpy.documents.Block current_block: Current block of the main blockchain + """ + self.blockid = current_block + self._metadata['time'] = current_block.mediantime + + def _be_validating(self, rollback, block): + """ + Action when the transfer ins found in a block + + :param bool rollback: True if we are in a rollback procedure + :param ucoinpy.documents.Block block: The block checked + """ + self.blockid = block.blockid + self._metadata['time'] = block.mediantime + + def _drop(self): + """ + Cancel the transfer locally. + The transfer state becomes TransferState.DROPPED. + """ + self.blockid = None + + def _try_transition(self, transition_key, inputs): + """ + Try the transition defined by the given transition_key + with inputs + :param tuple transition_key: The transition key in the table states + :param tuple inputs: The inputs + :return: True if the transition was applied + :rtype: bool + """ + if len(inputs) == len(transition_key[1]): + for i, input in enumerate(inputs): + if type(input) is not transition_key[1][i]: + return False + if self._table_states[transition_key][0](*inputs): + next_state = self._table_states[transition_key] + logging.debug("{0} : {1} --> {2}".format(self.sha_hash[:5], self.state.name, next_state[2].name)) + # If the transition changes data, apply changes + if next_state[1]: + next_state[1](*inputs) + self.state = next_state[2] + return True + return False + + def run_state_transitions(self, inputs): + """ + Try all current state transitions with inputs + :param tuple inputs: The inputs passed to the transitions + :return: True if the transaction changed state + :rtype: bool + """ + transition_keys = [k for k in self._table_states.keys() if k[0] == self.state] + for key in transition_keys: + if self._try_transition(key, inputs): + return True + return False @asyncio.coroutine def send(self, txdoc, community): @@ -112,13 +312,10 @@ class Transfer(QObject): """ responses = yield from community.bma_access.broadcast(bma.tx.Process, post_args={'transaction': txdoc.signed_raw()}) - self.state = Transfer.AWAITING - self.hash = hashlib.sha1(txdoc.signed_raw().encode("ascii")).hexdigest().upper() blockid = yield from community.blockid() block = yield from community.bma_access.future_request(bma.blockchain.Block, - req_args={'number': blockid['number']}) - self._metadata['block'] = blockid['number'] - self._metadata['time'] = block['medianTime'] + req_args={'number': blockid.number}) + time = block['medianTime'] result = (False, "") for r in responses: if r.status == 200: @@ -127,43 +324,5 @@ class Transfer(QObject): result = (False, (yield from r.text())) else: yield from r.text() - + self.run_state_transitions(([r.status for r in responses], time)) return result - - def check_registered(self, txhash, block_number, time, nb_validation): - """ - Check if the transfer was registered in a block. - Update the transfer state to VALIDATED if it was registered. - - :param txhash: A transaction ucoinpy object found in the block - :param int block_number: The block number checked - :param int time: The time of the block - :param int nb_validation: The number of validations needed to become VALIDATED - """ - if txhash == self.hash: - if self.state == Transfer.AWAITING: - self.state = Transfer.VALIDATING - self._metadata['block'] = block_number - self._metadata['time'] = time - if self.state == Transfer.VALIDATING and \ - self._metadata['block'] - block_number >= nb_validation: - self.state = Transfer.VALIDATED - - def check_refused(self, time, block_time, mediantime_blocks): - """ - Check if the transfer was refused - If more than block_time*15 seconds passed since - transfer, it is considered as refused. - - :param int block: The current block number - """ - if time > self._metadata['time'] + block_time*mediantime_blocks*10: - self.state = Transfer.REFUSED - - def drop(self): - """ - Cancel the transfer locally. - The transfer state becomes "DROPPED". - """ - self.state = Transfer.DROPPED - diff --git a/src/cutecoin/core/txhistory.py b/src/cutecoin/core/txhistory.py index 98bb117ab9195b90671de12fa14f1d45a2a3da81..d3cd3c08027ce79b3aa706756187d1962be3d515 100644 --- a/src/cutecoin/core/txhistory.py +++ b/src/cutecoin/core/txhistory.py @@ -1,7 +1,7 @@ import asyncio import logging import hashlib -from .transfer import Transfer +from .transfer import Transfer, TransferState from ucoinpy.documents.transaction import InputSource, OutputSource from ucoinpy.documents.block import Block from ..tools.exceptions import LookupFailureError, NoPeerAvailable @@ -39,6 +39,7 @@ class TxHistory(): self.available_sources.append(InputSource.from_inline(s['inline'])) for d in data['dividends']: + d['state'] = TransferState[d['state']] self._dividends.append(d) self.latest_block = data['latest_block'] @@ -54,7 +55,8 @@ class TxHistory(): data_sources.append({'inline': "{0}\n".format(s.inline())}) data_dividends = [] - for d in self._dividends: + for d in self._dividends.copy(): + d['state'] = d['state'].name data_dividends.append(d) return {'latest_block': self.latest_block, @@ -64,7 +66,7 @@ class TxHistory(): @property def transfers(self): - return [t for t in self._transfers if t.state != Transfer.DROPPED] + return [t for t in self._transfers if t.state != TransferState.DROPPED] @property def dividends(self): @@ -73,34 +75,22 @@ class TxHistory(): def stop_coroutines(self): self._stop_coroutines = True - @staticmethod @asyncio.coroutine - def _validation_state(community, block_number, current_block): - members_pubkeys = yield from community.members_pubkeys() - if block_number + community.network.fork_window(members_pubkeys) <= current_block["number"]: - state = Transfer.VALIDATED - else: - state = Transfer.VALIDATING - return state - - @asyncio.coroutine - def _parse_transaction(self, community, tx, block_number, - mediantime, received_list, - current_block, txid): + def _parse_transaction(self, community, tx, blockid, + mediantime, received_list, txid): """ Parse a transaction :param cutecoin.core.Community community: The community :param ucoinpy.documents.Transaction tx: The tx json data - :param int block_number: The block number were we found the tx + :param ucoinpy.documents.BlockId blockid: The block id where we found the tx :param int mediantime: Median time on the network :param list received_list: The list of received transactions - :param int current_block: The current block of the network :param int txid: The latest txid :return: the found transaction """ receivers = [o.pubkey for o in tx.outputs if o.pubkey != tx.issuers[0]] - nb_validations = community.network.fork_window((yield from community.members_pubkeys())) + if len(receivers) == 0: receivers = [tx.issuers[0]] @@ -116,58 +106,51 @@ class TxHistory(): except LookupFailureError: receiver_uid = "" - metadata = {'block': block_number, + metadata = { 'time': mediantime, 'comment': tx.comment, 'issuer': tx.issuers[0], 'issuer_uid': issuer_uid, 'receiver': receivers[0], 'receiver_uid': receiver_uid, - 'txid': txid} + 'txid': txid + } in_issuers = len([i for i in tx.issuers if i == self.wallet.pubkey]) > 0 in_outputs = len([o for o in tx.outputs if o.pubkey == self.wallet.pubkey]) > 0 - watched = [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 tx_hash = hashlib.sha1(tx.signed_raw().encode("ascii")).hexdigest().upper() - if tx_hash not in [t.hash for t in watched]: - # 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(tx_hash, - metadata.copy(), - current_block['number'], - mediantime, nb_validations) - 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 - transfer = Transfer.create_from_blockchain(tx_hash, - metadata.copy(), - current_block['number'], - mediantime, nb_validations) - received_list.append(transfer) - return transfer - else: - transfer = [t for t in watched if t.hash == tx_hash][0] - - transfer.check_registered(tx_hash, current_block['number'], mediantime, nb_validations) + # 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(tx_hash, + blockid, + 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 + + transfer = Transfer.create_from_blockchain(tx_hash, + blockid, + metadata.copy()) + received_list.append(transfer) + return transfer return None @asyncio.coroutine @@ -202,10 +185,16 @@ class TxHistory(): block = None tries += 1 if block_doc: - for (txid, tx) in enumerate(block_doc.transactions): - transfer = yield from self._parse_transaction(community, tx, block_number, - block_doc.mediantime, received_list, - current_block, txid+txmax) + for transfer in [t for t in self._transfers if t.state == TransferState.AWAITING]: + transfer.run_state_transitions((False, block_doc)) + + new_tx = [t for t in block_doc.transactions + if t.sha_hash not in [trans.sha_hash for trans in self._transfers] + ] + + for (txid, tx) in enumerate(new_tx): + transfer = yield from self._parse_transaction(community, tx, block_doc.blockid, + block_doc.mediantime, received_list, txid+txmax) if transfer != None: #logging.debug("Transfer amount : {0}".format(transfer.metadata['amount'])) transfers.append(transfer) @@ -223,7 +212,8 @@ class TxHistory(): dividends_data = yield from community.bma_access.future_request(bma.ud.History, req_args={'pubkey': self.wallet.pubkey}) - dividends = dividends_data['history']['history'] + dividends = dividends_data['history']['history'].copy() + for d in dividends: if d['block_number'] < parsed_block: dividends.remove(d) @@ -245,21 +235,25 @@ class TxHistory(): logging.debug("Refresh from : {0} to {1}".format(block_number_from, self._block_to['number'])) dividends = yield from self.request_dividends(community, block_number_from) with_tx_data = yield from community.bma_access.future_request(bma.blockchain.TX) + members_pubkeys = yield from community.members_pubkeys() + fork_window = community.network.fork_window(members_pubkeys) blocks_with_tx = with_tx_data['result']['blocks'] new_transfers = [] new_dividends = [] # Lets look if transactions took too long to be validated awaiting = [t for t in self._transfers - if t.state == Transfer.AWAITING] + if t.state == TransferState.AWAITING] while block_number_from <= self._block_to['number']: udid = 0 for d in [ud for ud in dividends if ud['block_number'] == block_number_from]: - state = yield from TxHistory._validation_state(community, d['block_number'], self._block_to) + state = TransferState.VALIDATED if block_number_from + fork_window <= self._block_to['number'] \ + else TransferState.VALIDATING if d['block_number'] not in [ud['block_number'] for ud in self._dividends]: d['id'] = udid d['state'] = state new_dividends.append(d) + udid += 1 else: known_dividend = [ud for ud in self._dividends @@ -268,11 +262,6 @@ class TxHistory(): # We parse only blocks with transactions if block_number_from in blocks_with_tx: - # We check if validated transfers should go back to validating... - for t in [t for t in self._transfers if t.metadata['block'] == block_number_from]: - t.state = yield from TxHistory._validation_state(community, t.metadata['block'], - self._block_to) - transfers = yield from self._parse_block(community, block_number_from, received_list, self._block_to, udid + len(new_transfers)) @@ -281,6 +270,12 @@ class TxHistory(): self.wallet.refresh_progressed.emit(block_number_from, self._block_to['number'], self.wallet.pubkey) block_number_from += 1 + signed_raw = "{0}{1}\n".format(self._block_to['raw'], + self._block_to['signature']) + block_to = Block.from_signed_raw(signed_raw) + for transfer in [t for t in self._transfers if t.state == TransferState.VALIDATING]: + transfer.run_state_transitions((False, block_to, fork_window)) + # We check if latest parsed block_number is a new high number if block_number_from > self.latest_block: self.available_sources = yield from self.wallet.sources(community) @@ -313,10 +308,12 @@ class TxHistory(): req_args={'number': latest_block_number}) members_pubkeys = yield from community.members_pubkeys() # We look for the first block to parse, depending on awaiting and validating transfers and ud... - blocks = [tx.metadata['block'] for tx in self._transfers - if tx.state in (Transfer.AWAITING, Transfer.VALIDATING)] +\ - [ud['block_number'] for ud in self._dividends - if ud['state'] in (Transfer.AWAITING, Transfer.VALIDATING)] +\ + tx_blocks = [tx.blockid.number for tx in self._transfers + if tx.state in (TransferState.AWAITING, TransferState.VALIDATING) \ + and tx.blockid is not None] + ud_blocks = [ud['block_number'] for ud in self._dividends + if ud['state'] in (TransferState.AWAITING, TransferState.VALIDATING)] + blocks = tx_blocks + ud_blocks + \ [max(0, self.latest_block - community.network.fork_window(members_pubkeys))] parsed_block = min(set(blocks)) self._block_to = current_block diff --git a/src/cutecoin/models/txhistory.py b/src/cutecoin/models/txhistory.py index 482fed11b3c063ae61790717a2342a33312ad9f7..4e434d6158ff492736bd50f1efb49a5802cc51f0 100644 --- a/src/cutecoin/models/txhistory.py +++ b/src/cutecoin/models/txhistory.py @@ -7,7 +7,7 @@ Created on 5 févr. 2014 import datetime import logging import asyncio -from ..core.transfer import Transfer +from ..core.transfer import Transfer, TransferState from ..tools.exceptions import NoPeerAvailable from ..tools.decorators import asyncify, once_at_a_time, cancel_once_task from PyQt5.QtCore import QAbstractTableModel, Qt, QVariant, QSortFilterProxyModel, \ @@ -119,20 +119,20 @@ class TxFilterProxyModel(QSortFilterProxyModel): if role == Qt.FontRole: font = QFont() - if state_data == Transfer.AWAITING or state_data == Transfer.VALIDATING: + if state_data == TransferState.AWAITING or state_data == TransferState.VALIDATING: font.setItalic(True) - elif state_data == Transfer.REFUSED: + elif state_data == TransferState.REFUSED: font.setItalic(True) - elif state_data == Transfer.TO_SEND: + elif state_data == TransferState.TO_SEND: font.setBold(True) else: font.setItalic(False) return font if role == Qt.ForegroundRole: - if state_data == Transfer.REFUSED: + if state_data == TransferState.REFUSED: return QColor(Qt.red) - elif state_data == Transfer.TO_SEND: + elif state_data == TransferState.TO_SEND: return QColor(Qt.blue) if role == Qt.TextAlignmentRole: @@ -146,13 +146,13 @@ class TxFilterProxyModel(QSortFilterProxyModel): if source_index.column() == self.sourceModel().columns_types.index('date'): return QDateTime.fromTime_t(source_data).toString(Qt.SystemLocaleLongDate) - if state_data == Transfer.VALIDATING or state_data == Transfer.AWAITING: + if state_data == TransferState.VALIDATING or state_data == TransferState.AWAITING: 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) current_validations = 0 - if state_data == Transfer.VALIDATING: + if state_data == TransferState.VALIDATING: latest_block_number = self.community.network.latest_block_number if latest_block_number: current_validations = latest_block_number - block_data @@ -241,7 +241,10 @@ class HistoryTableModel(QAbstractTableModel): date_ts = transfer.metadata['time'] txid = transfer.metadata['txid'] - block_number = transfer.metadata['block'] + if transfer.blockid: + block_number = transfer.blockid.number + else: + block_number = None return (date_ts, sender, "", deposit, comment, transfer.state, txid, @@ -262,7 +265,10 @@ class HistoryTableModel(QAbstractTableModel): date_ts = transfer.metadata['time'] txid = transfer.metadata['txid'] - block_number = transfer.metadata['block'] + if transfer.blockid: + block_number = transfer.blockid.number + else: + block_number = None return (date_ts, receiver, paiment, "", comment, transfer.state, txid,