diff --git a/bigchaindb/models.py b/bigchaindb/models.py index 4bc9df23..8d9aae6f 100644 --- a/bigchaindb/models.py +++ b/bigchaindb/models.py @@ -1,14 +1,10 @@ -from copy import deepcopy - -from bigchaindb.common.crypto import hash_data, PublicKey, PrivateKey -from bigchaindb.common.exceptions import (InvalidHash, InvalidSignature, - DoubleSpend, InputDoesNotExist, +from bigchaindb.common.exceptions import (InvalidSignature, DoubleSpend, + InputDoesNotExist, TransactionNotInValidBlock, AssetIdMismatch, AmountError, DuplicateTransaction) from bigchaindb.common.transaction import Transaction -from bigchaindb.common.utils import (gen_timestamp, serialize, - validate_txn_obj, validate_key) +from bigchaindb.common.utils import (validate_txn_obj, validate_key) from bigchaindb.common.schema import validate_transaction_schema from bigchaindb.backend.schema import validate_language_key @@ -156,368 +152,6 @@ class Transaction(Transaction): return cls.from_dict(tx) -# TODO: Remove node_pubkey as part of cleanup II -class Block(object): - """Bundle a list of Transactions in a Block. - - Attributes: - transaction (:obj:`list` of :class:`~.Transaction`): - Transactions to be included in the Block. - node_pubkey (str): The public key of the node creating the - Block. - timestamp (str): The Unix time a Block was created. - signature (str): A cryptographic signature ensuring the - integrity and validity of the creator of a Block. - """ - - def __init__(self, transactions=None, node_pubkey=None, timestamp=None, - signature=None): - """The Block model is mainly used for (de)serialization and integrity - checking. - - Args: - transaction (:obj:`list` of :class:`~.Transaction`): - Transactions to be included in the Block. - node_pubkey (str): The public key of the node creating the - Block. - timestamp (str): The Unix time a Block was created. - signature (str): A cryptographic signature ensuring the - integrity and validity of the creator of a Block. - """ - if transactions is not None and not isinstance(transactions, list): - raise TypeError('`transactions` must be a list instance or None') - else: - self.transactions = transactions or [] - - if timestamp is not None: - self.timestamp = timestamp - else: - self.timestamp = gen_timestamp() - - self.node_pubkey = node_pubkey - self.signature = signature - - def __eq__(self, other): - try: - other = other.to_dict() - except AttributeError: - return False - return self.to_dict() == other - - def validate(self, bigchain): - """Validate the Block. - - Args: - bigchain (:class:`~bigchaindb.Bigchain`): An instantiated Bigchain - object. - - Note: - The hash of the block (`id`) is validated on the `self.from_dict` - method. This is because the `from_dict` is the only method in - which we have the original json payload. The `id` provided by - this class is a mutable property that is generated on the fly. - - Returns: - :class:`~.Block`: If valid, return a `Block` object. Else an - appropriate exception describing the reason of invalidity is - raised. - - Raises: - ValidationError: If the block or any transaction in the block does - not validate - """ - - self._validate_block(bigchain) - self._validate_block_transactions(bigchain) - - return self - - def _validate_block(self, bigchain): - """Validate the Block without validating the transactions. - - Args: - bigchain (:class:`~bigchaindb.Bigchain`): An instantiated Bigchain - object. - - Raises: - ValidationError: If there is a problem with the block - """ - # Check that the signature is valid - if not self.is_signature_valid(): - raise InvalidSignature('Invalid block signature') - - # Check that the block contains no duplicated transactions - txids = [tx.id for tx in self.transactions] - if len(txids) != len(set(txids)): - raise DuplicateTransaction('Block has duplicate transaction') - - def _validate_block_transactions(self, bigchain): - """Validate Block transactions. - - Args: - bigchain (Bigchain): an instantiated bigchaindb.Bigchain object. - - Raises: - ValidationError: If an invalid transaction is found - """ - for tx in self.transactions: - # If a transaction is not valid, `validate_transactions` will - # throw an an exception and block validation will be canceled. - bigchain.validate_transaction(tx) - - def sign(self, private_key): - """Create a signature for the Block and overwrite `self.signature`. - - Args: - private_key (str): A private key corresponding to - `self.node_pubkey`. - - Returns: - :class:`~.Block` - """ - block_body = self.to_dict() - block_serialized = serialize(block_body['block']) - private_key = PrivateKey(private_key) - self.signature = private_key.sign(block_serialized.encode()).decode() - return self - - def is_signature_valid(self): - """Check the validity of a Block's signature. - - Returns: - bool: Stating the validity of the Block's signature. - """ - block = self.to_dict()['block'] - # cc only accepts bytestring messages - block_serialized = serialize(block).encode() - public_key = PublicKey(block['node_pubkey']) - try: - # NOTE: CC throws a `ValueError` on some wrong signatures - # https://github.com/bigchaindb/cryptoconditions/issues/27 - return public_key.verify(block_serialized, self.signature) - except (ValueError, AttributeError): - return False - - @classmethod - def from_dict(cls, block_body, tx_construct=Transaction.from_dict): - """Transform a Python dictionary to a Block object. - - Args: - block_body (dict): A block dictionary to be transformed. - tx_construct (functions): Function to instantiate Transaction instance - - Returns: - :class:`~Block` - - Raises: - InvalidHash: If the block's id is not corresponding to its - data. - """ - # Validate block id - block = block_body['block'] - block_serialized = serialize(block) - block_id = hash_data(block_serialized) - - if block_id != block_body['id']: - raise InvalidHash() - - transactions = [tx_construct(tx) for tx in block['transactions']] - - signature = block_body.get('signature') - - return cls(transactions, block['node_pubkey'], - block['timestamp'], signature) - - @property - def id(self): - return self.to_dict()['id'] - - def to_dict(self): - """Transform the Block to a Python dictionary. - - Returns: - dict: The Block as a dict. - - Raises: - ValueError: If the Block doesn't contain any transactions. - """ - if len(self.transactions) == 0: - raise ValueError('Empty block creation is not allowed') - - block = { - 'timestamp': self.timestamp, - 'transactions': [tx.to_dict() for tx in self.transactions], - 'node_pubkey': self.node_pubkey, - } - block_serialized = serialize(block) - block_id = hash_data(block_serialized) - - return { - 'id': block_id, - 'block': block, - 'signature': self.signature, - } - - @classmethod - def from_db(cls, bigchain, block_dict, from_dict_kwargs=None): - """Helper method that reconstructs a block_dict that was returned from - the database. It checks what asset_ids to retrieve, retrieves the - assets from the assets table and reconstructs the block. - - Args: - bigchain (:class:`~bigchaindb.Bigchain`): An instance of Bigchain - used to perform database queries. - block_dict(:obj:`dict`): The block dict as returned from the - database. - from_dict_kwargs (:obj:`dict`): additional kwargs to pass to from_dict - - Returns: - :class:`~Block` - - """ - asset_ids = cls.get_asset_ids(block_dict) - assets = bigchain.get_assets(asset_ids) - txn_ids = cls.get_txn_ids(block_dict) - metadata = bigchain.get_metadata(txn_ids) - # reconstruct block - block_dict = cls.couple_assets(block_dict, assets) - block_dict = cls.couple_metadata(block_dict, metadata) - kwargs = from_dict_kwargs or {} - return cls.from_dict(block_dict, **kwargs) - - def decouple_assets(self, block_dict=None): - """Extracts the assets from the ``CREATE`` transactions in the block. - - Returns: - tuple: (assets, block) with the assets being a list of dicts and - the block being the dict of the block with no assets in the CREATE - transactions. - """ - if block_dict is None: - block_dict = deepcopy(self.to_dict()) - - assets = [] - for transaction in block_dict['block']['transactions']: - if transaction['operation'] == Transaction.CREATE: - asset = transaction.pop('asset') - asset.update({'id': transaction['id']}) - assets.append(asset) - - return (assets, block_dict) - - def decouple_metadata(self, block_dict=None): - """Extracts the metadata from transactions in the block. - - Returns: - tuple: (metadatas, block) with the metadatas being a list of dict/null and - the block being the dict of the block with no metadata in any transaction. - """ - if block_dict is None: - block_dict = deepcopy(self.to_dict()) - - metadatas = [] - for transaction in block_dict['block']['transactions']: - metadata = transaction.pop('metadata') - if metadata: - metadata_new = {'id': transaction['id'], - 'metadata': metadata} - metadatas.append(metadata_new) - - return (metadatas, block_dict) - - @staticmethod - def couple_assets(block_dict, assets): - """Given a block_dict with no assets (as returned from a database call) - and a list of assets, reconstruct the original block by putting the - assets back into the ``CREATE`` transactions in the block. - - Args: - block_dict (:obj:`dict`): The block dict as returned from a - database call. - assets (:obj:`list` of :obj:`dict`): A list of assets returned from - a database call. - - Returns: - dict: The dict of the reconstructed block. - """ - # create a dict with {'': asset} - assets = {asset.pop('id'): asset for asset in assets} - # add the assets to the block transactions - for transaction in block_dict['block']['transactions']: - if transaction['operation'] == Transaction.CREATE: - transaction.update({'asset': assets.get(transaction['id'])}) - return block_dict - - @staticmethod - def couple_metadata(block_dict, metadatal): - """Given a block_dict with no metadata (as returned from a database call) - and a list of metadata, reconstruct the original block by putting the - metadata of each transaction back into its original transaction. - - NOTE: Till a transaction gets accepted the `metadata` of the transaction - is not moved outside of the transaction. So, if a transaction is found to - have metadata then it should not be overridden. - - Args: - block_dict (:obj:`dict`): The block dict as returned from a - database call. - metadata (:obj:`list` of :obj:`dict`): A list of metadata returned from - a database call. - - Returns: - dict: The dict of the reconstructed block. - """ - # create a dict with {'': metadata} - metadatal = {m.pop('id'): m.pop('metadata') for m in metadatal} - # add the metadata to their corresponding transactions - for transaction in block_dict['block']['transactions']: - metadata = metadatal.get(transaction['id'], None) - transaction.update({'metadata': metadata}) - return block_dict - - @staticmethod - def get_asset_ids(block_dict): - """Given a block_dict return all the asset_ids for that block (the txid - of CREATE transactions). Useful to know which assets to retrieve - from the database to reconstruct the block. - - Args: - block_dict (:obj:`dict`): The block dict as returned from a - database call. - - Returns: - list: The list of asset_ids in the block. - - """ - asset_ids = [] - for transaction in block_dict['block']['transactions']: - if transaction['operation'] == Transaction.CREATE: - asset_ids.append(transaction['id']) - - return asset_ids - - @staticmethod - def get_txn_ids(block_dict): - """Given a block_dict return all the transaction ids. - - Args: - block_dict (:obj:`dict`): The block dict as returned from a - database call. - - Returns: - list: The list of txn_ids in the block. - - """ - txn_ids = [] - for transaction in block_dict['block']['transactions']: - txn_ids.append(transaction['id']) - - return txn_ids - - def to_str(self): - return serialize(self.to_dict()) - - class FastTransaction: """A minimal wrapper around a transaction dictionary. This is useful for when validation is not required but a routine expects something that looks diff --git a/docs/server/generate_http_server_api_documentation.py b/docs/server/generate_http_server_api_documentation.py index 31403da5..69bcd699 100644 --- a/docs/server/generate_http_server_api_documentation.py +++ b/docs/server/generate_http_server_api_documentation.py @@ -6,7 +6,6 @@ import os.path from bigchaindb.common.transaction import Transaction, Input, TransactionLink from bigchaindb.core import Bigchain -from bigchaindb.models import Block from bigchaindb.tendermint import lib from bigchaindb.web import server diff --git a/tests/db/test_bigchain_api.py b/tests/db/test_bigchain_api.py index bfa2c1b2..91d9cae6 100644 --- a/tests/db/test_bigchain_api.py +++ b/tests/db/test_bigchain_api.py @@ -433,17 +433,6 @@ class TestBigchainApi(object): assert excinfo.value.args[0] == 'Empty block creation is not allowed' - @pytest.mark.genesis - def test_get_last_voted_block_returns_genesis_if_no_votes_has_been_casted(self, b): - from bigchaindb.models import Block - from bigchaindb.backend import query - - genesis = query.get_genesis_block(b.connection) - genesis = Block.from_db(b, genesis) - gb = b.get_last_voted_block() - assert gb == genesis - assert b.validate_block(gb) == gb - @pytest.mark.usefixtures('inputs') def test_non_create_input_not_found(self, b, user_pk): from cryptoconditions import Ed25519Sha256 diff --git a/tests/test_fastquery.py b/tests/test_fastquery.py index 9f1d1449..e6c6dfbe 100644 --- a/tests/test_fastquery.py +++ b/tests/test_fastquery.py @@ -1,26 +1,10 @@ import pytest from bigchaindb.common.transaction import TransactionLink -from bigchaindb.models import Block, Transaction pytestmark = pytest.mark.bdb -@pytest.fixture -def blockdata(b, user_pk, user2_pk): - txs = [Transaction.create([user_pk], [([user2_pk], 1)]), - Transaction.create([user2_pk], [([user_pk], 1)]), - Transaction.create([user_pk], [([user_pk], 1), ([user2_pk], 1)])] - blocks = [] - for i in range(3): - block = Block([txs[i]]) - b.write_block(block) - blocks.append(block.to_dict()) - b.write_vote(b.vote(blocks[1]['id'], '', True)) - b.write_vote(b.vote(blocks[2]['id'], '', False)) - return blocks, [b['id'] for b in blocks] - - def test_filter_valid_items(b, blockdata): blocks, _ = blockdata assert (b.fastquery.filter_valid_items(blocks, block_id_key=lambda b: b['id']) @@ -35,83 +19,3 @@ def test_get_outputs_by_public_key(b, user_pk, user2_pk, blockdata): assert b.fastquery.get_outputs_by_public_key(user2_pk) == [ TransactionLink(blocks[0]['block']['transactions'][0]['id'], 0) ] - - -def test_filter_spent_outputs(b, user_pk, user_sk): - out = [([user_pk], 1)] - tx1 = Transaction.create([user_pk], out * 3) - tx1.sign([user_sk]) - # There are 3 inputs - inputs = tx1.to_inputs() - - # Each spent individually - tx2 = Transaction.transfer([inputs[0]], out, tx1.id) - tx2.sign([user_sk]) - tx3 = Transaction.transfer([inputs[1]], out, tx1.id) - tx3.sign([user_sk]) - tx4 = Transaction.transfer([inputs[2]], out, tx1.id) - tx4.sign([user_sk]) - - # The CREATE and first TRANSFER are valid. tx2 produces a new unspent. - for tx in [tx1, tx2]: - block = Block([tx]) - b.write_block(block) - b.write_vote(b.vote(block.id, '', True)) - - # The second TRANSFER is invalid. inputs[1] remains unspent. - block = Block([tx3]) - b.write_block(block) - b.write_vote(b.vote(block.id, '', False)) - - # The third TRANSFER is undecided. It procuces a new unspent. - block = Block([tx4]) - b.write_block(block) - - outputs = b.fastquery.get_outputs_by_public_key(user_pk) - unspents = b.fastquery.filter_spent_outputs(outputs) - - assert set(unspents) == { - inputs[1].fulfills, - tx2.to_inputs()[0].fulfills, - tx4.to_inputs()[0].fulfills - } - - -def test_filter_unspent_outputs(b, user_pk, user_sk): - out = [([user_pk], 1)] - tx1 = Transaction.create([user_pk], out * 3) - tx1.sign([user_sk]) - - # There are 3 inputs - inputs = tx1.to_inputs() - - # Each spent individually - tx2 = Transaction.transfer([inputs[0]], out, tx1.id) - tx2.sign([user_sk]) - tx3 = Transaction.transfer([inputs[1]], out, tx1.id) - tx3.sign([user_sk]) - tx4 = Transaction.transfer([inputs[2]], out, tx1.id) - tx4.sign([user_sk]) - - # The CREATE and first TRANSFER are valid. tx2 produces a new unspent. - for tx in [tx1, tx2]: - block = Block([tx]) - b.write_block(block) - b.write_vote(b.vote(block.id, '', True)) - - # The second TRANSFER is invalid. inputs[1] remains unspent. - block = Block([tx3]) - b.write_block(block) - b.write_vote(b.vote(block.id, '', False)) - - # The third TRANSFER is undecided. It procuces a new unspent. - block = Block([tx4]) - b.write_block(block) - - outputs = b.fastquery.get_outputs_by_public_key(user_pk) - spents = b.fastquery.filter_unspent_outputs(outputs) - - assert set(spents) == { - inputs[0].fulfills, - inputs[2].fulfills - }