diff --git a/bigchaindb/assets.py b/bigchaindb/assets.py index ebe1b337..e69de29b 100644 --- a/bigchaindb/assets.py +++ b/bigchaindb/assets.py @@ -1,42 +0,0 @@ -import rethinkdb as r - -from bigchaindb_common.exceptions import AssetIdMismatch, AmountError - - -def get_asset_id(transactions): - """Get the asset id from a list of transaction ids. - - This is useful when we want to check if the multiple inputs of a transaction - are related to the same asset id. - - Args: - transactions (list): list of transaction usually inputs that should have a matching asset_id - - Returns: - str: uuid of the asset. - - Raises: - AssetIdMismatch: If the inputs are related to different assets. - """ - - if not isinstance(transactions, list): - transactions = [transactions] - - # create a set of asset_ids - asset_ids = {tx['transaction']['asset']['id'] for tx in transactions} - - # check that all the transasctions have the same asset_id - if len(asset_ids) > 1: - raise AssetIdMismatch("All inputs of a transaction need to have the same asset id.") - return asset_ids.pop() - - -def get_transactions_by_asset_id(asset_id, bigchain, read_mode='majority'): - cursor = r.table('bigchain', read_mode=read_mode)\ - .get_all(asset_id, index='asset_id')\ - .concat_map(lambda block: block['block']['transactions'])\ - .filter(lambda transaction: transaction['transaction']['asset']['id'] == asset_id)\ - .run(bigchain.conn) - - transactions = list(cursor) - return transactions diff --git a/bigchaindb/core.py b/bigchaindb/core.py index 4a945e42..5a70496c 100644 --- a/bigchaindb/core.py +++ b/bigchaindb/core.py @@ -364,7 +364,13 @@ class Bigchain(object): A list of transactions containing related to the asset. If no transaction exists for that asset it returns an empty list `[]` """ - return assets.get_transactions_by_asset_id(asset_id, self, read_mode=self.read_mode) + cursor = self.connection.run( + r.table('bigchain', read_mode=self.read_mode) + .get_all(asset_id, index='asset_id') + .concat_map(lambda block: block['block']['transactions']) + .filter(lambda transaction: transaction['transaction']['asset']['id'] == asset_id)) + + return [Transaction.from_dict(tx) for tx in cursor] def get_spent(self, txid, cid): """Check if a `txid` was already used as an input. diff --git a/bigchaindb/models.py b/bigchaindb/models.py index 6855787e..3b708b78 100644 --- a/bigchaindb/models.py +++ b/bigchaindb/models.py @@ -1,11 +1,42 @@ from bigchaindb_common.crypto import hash_data, VerifyingKey, SigningKey from bigchaindb_common.exceptions import (InvalidHash, InvalidSignature, OperationError, DoubleSpend, - TransactionDoesNotExist) -from bigchaindb_common.transaction import Transaction + TransactionDoesNotExist, + AssetIdMismatch) +from bigchaindb_common.transaction import Transaction, Asset from bigchaindb_common.util import gen_timestamp, serialize +class Asset(Asset): + @staticmethod + def get_asset_id(transactions): + """Get the asset id from a list of transaction ids. + + This is useful when we want to check if the multiple inputs of a transaction + are related to the same asset id. + + Args: + transactions (list): list of transaction usually inputs that should have a matching asset_id + + Returns: + str: uuid of the asset. + + Raises: + AssetIdMismatch: If the inputs are related to different assets. + """ + + if not isinstance(transactions, list): + transactions = [transactions] + + # create a set of asset_ids + asset_ids = {tx.asset.data_id for tx in transactions} + + # check that all the transasctions have the same asset_id + if len(asset_ids) > 1: + raise AssetIdMismatch("All inputs of a transaction need to have the same asset id.") + return asset_ids.pop() + + class Transaction(Transaction): def validate(self, bigchain): """Validate a transaction. @@ -35,13 +66,18 @@ class Transaction(Transaction): inputs_defined = all([ffill.tx_input for ffill in self.fulfillments]) if self.operation in (Transaction.CREATE, Transaction.GENESIS): + # validate inputs if inputs_defined: raise ValueError('A CREATE operation has no inputs') + # validate asset + self.asset._validate_asset() elif self.operation == Transaction.TRANSFER: if not inputs_defined: raise ValueError('Only `CREATE` transactions can have null ' 'inputs') - + # check inputs + # store the inputs so that we can check if the asset ids match + input_txs = [] for ffill in self.fulfillments: input_txid = ffill.tx_input.txid input_cid = ffill.tx_input.cid @@ -56,6 +92,12 @@ class Transaction(Transaction): .format(input_txid)) input_conditions.append(input_tx.conditions[input_cid]) + input_txs.append(input_tx) + + # validate asset id + asset_id = Asset.get_asset_id(input_txs) + if asset_id != self.asset.data_id: + raise AssetIdMismatch('The asset id of the input does not match the asset id of the transaction') else: allowed_operations = ', '.join(Transaction.ALLOWED_OPERATIONS) raise TypeError('`operation`: `{}` must be either {}.' diff --git a/tests/assets/test_digital_assets.py b/tests/assets/test_digital_assets.py index ad028260..73085008 100644 --- a/tests/assets/test_digital_assets.py +++ b/tests/assets/test_digital_assets.py @@ -20,33 +20,36 @@ def test_validate_bad_asset_creation(b, user_vk): from bigchaindb_common.exceptions import AmountError from bigchaindb.models import Transaction + # `divisible` needs to be a boolean tx = Transaction.create([b.me], [user_vk]) tx.asset.divisible = 1 tx_signed = tx.sign([b.me_private]) with pytest.raises(TypeError): tx_signed.validate(b) - tx = b.create_transaction(b.me, user_vk, None, 'CREATE') - tx['transaction']['asset'].update({'refillable': 1}) - tx['id'] = get_hash_data(tx['transaction']) - tx_signed = b.sign_transaction(tx, b.me_private) + # `refillable` needs to be a boolean + tx = Transaction.create([b.me], [user_vk]) + tx.asset.refillable = 1 + tx_signed = tx.sign([b.me_private]) with pytest.raises(TypeError): b.validate_transaction(tx_signed) - tx = b.create_transaction(b.me, user_vk, None, 'CREATE') - tx['transaction']['asset'].update({'updatable': 1}) - tx['id'] = get_hash_data(tx['transaction']) - tx_signed = b.sign_transaction(tx, b.me_private) + # `updatable` needs to be a boolean + tx = Transaction.create([b.me], [user_vk]) + tx.asset.updatable = 1 + tx_signed = tx.sign([b.me_private]) with pytest.raises(TypeError): b.validate_transaction(tx_signed) - tx = b.create_transaction(b.me, user_vk, None, 'CREATE') - tx['transaction']['asset'].update({'data': 'a'}) - tx['id'] = get_hash_data(tx['transaction']) - tx_signed = b.sign_transaction(tx, b.me_private) + # `data` needs to be a dictionary + tx = Transaction.create([b.me], [user_vk]) + tx.asset.data = 'a' + tx_signed = tx.sign([b.me_private]) with pytest.raises(TypeError): b.validate_transaction(tx_signed) + # TODO: Check where to test for the amount + """ tx = b.create_transaction(b.me, user_vk, None, 'CREATE') tx['transaction']['conditions'][0]['amount'] = 'a' tx['id'] = get_hash_data(tx['transaction']) @@ -68,116 +71,93 @@ def test_validate_bad_asset_creation(b, user_vk): tx_signed = b.sign_transaction(tx, b.me_private) with pytest.raises(AmountError): b.validate_transaction(tx_signed) - + """ @pytest.mark.usefixtures('inputs') def test_validate_transfer_asset_id_mismatch(b, user_vk, user_sk): - from bigchaindb.util import get_hash_data from bigchaindb_common.exceptions import AssetIdMismatch + from bigchaindb.models import Transaction - tx_input = b.get_owned_ids(user_vk).pop() - tx = b.create_transaction(user_vk, user_vk, tx_input, 'TRANFER') - tx['transaction']['asset']['id'] = 'aaa' - tx['id'] = get_hash_data(tx['transaction']) - tx_signed = b.sign_transaction(tx, user_sk) + tx_create = b.get_owned_ids(user_vk).pop() + tx_create = b.get_transaction(tx_create.txid) + tx_transfer = Transaction.transfer(tx_create.to_inputs(), [user_vk], tx_create.asset) + tx_transfer.asset.data_id = 'aaa' + tx_transfer_signed = tx_transfer.sign([user_sk]) with pytest.raises(AssetIdMismatch): - b.validate_transaction(tx_signed) - - -def test_validate_asset_arguments(b): - from bigchaindb_common.exceptions import AmountError - - with pytest.raises(TypeError): - b.create_transaction(b.me, b.me, None, 'CREATE', divisible=1) - with pytest.raises(TypeError): - b.create_transaction(b.me, b.me, None, 'CREATE', refillable=1) - with pytest.raises(TypeError): - b.create_transaction(b.me, b.me, None, 'CREATE', updatable=1) - with pytest.raises(TypeError): - b.create_transaction(b.me, b.me, None, 'CREATE', data='a') - with pytest.raises(TypeError): - b.create_transaction(b.me, b.me, None, 'CREATE', amount='a') - with pytest.raises(AmountError): - b.create_transaction(b.me, b.me, None, 'CREATE', divisible=False, amount=2) - with pytest.raises(AmountError): - b.create_transaction(b.me, b.me, None, 'CREATE', amount=0) + tx_transfer_signed.validate(b) @pytest.mark.usefixtures('inputs') def test_get_asset_id_create_transaction(b, user_vk): - from bigchaindb.assets import get_asset_id + from bigchaindb.models import Asset - tx_input = b.get_owned_ids(user_vk).pop() - tx_create = b.get_transaction(tx_input['txid']) - asset_id = get_asset_id(tx_create) + tx_create = b.get_owned_ids(user_vk).pop() + tx_create = b.get_transaction(tx_create.txid) + asset_id = Asset.get_asset_id(tx_create) - assert asset_id == tx_create['transaction']['asset']['id'] + assert asset_id == tx_create.asset.data_id @pytest.mark.usefixtures('inputs') def test_get_asset_id_transfer_transaction(b, user_vk, user_sk): - from bigchaindb.assets import get_asset_id + from bigchaindb.models import Transaction, Asset - tx_input = b.get_owned_ids(user_vk).pop() + tx_create = b.get_owned_ids(user_vk).pop() + tx_create = b.get_transaction(tx_create.txid) # create a transfer transaction - tx_transfer = b.create_transaction(user_vk, user_vk, tx_input, 'TRANSFER') - tx_transfer_signed = b.sign_transaction(tx_transfer, user_sk) + tx_transfer = Transaction.transfer(tx_create.to_inputs(), [user_vk], tx_create.asset) + tx_transfer_signed = tx_transfer.sign([user_sk]) # create a block block = b.create_block([tx_transfer_signed]) b.write_block(block, durability='hard') # vote the block valid - vote = b.vote(block['id'], b.get_last_voted_block()['id'], True) + vote = b.vote(block.id, b.get_last_voted_block().id, True) b.write_vote(vote) - asset_id = get_asset_id(tx_transfer) + asset_id = Asset.get_asset_id(tx_transfer) - assert asset_id == tx_transfer['transaction']['asset']['id'] + assert asset_id == tx_transfer.asset.data_id @pytest.mark.usefixtures('inputs') def test_asset_id_mismatch(b, user_vk): - from bigchaindb.assets import get_asset_id + from bigchaindb.models import Asset from bigchaindb_common.exceptions import AssetIdMismatch tx_input1, tx_input2 = b.get_owned_ids(user_vk)[:2] - tx1 = b.get_transaction(tx_input1['txid']) - tx2 = b.get_transaction(tx_input2['txid']) + tx1 = b.get_transaction(tx_input1.txid) + tx2 = b.get_transaction(tx_input2.txid) with pytest.raises(AssetIdMismatch): - get_asset_id([tx1, tx2]) - - -def test_get_asset_id_transaction_does_not_exist(b, user_vk): - from bigchaindb_common.exceptions import TransactionDoesNotExist - - with pytest.raises(TransactionDoesNotExist): - b.create_transaction(user_vk, user_vk, {'txid': 'bored', 'cid': '0'}, 'TRANSFER') + Asset.get_asset_id([tx1, tx2]) @pytest.mark.usefixtures('inputs') def test_get_txs_by_asset_id(b, user_vk, user_sk): - tx_input = b.get_owned_ids(user_vk).pop() - tx = b.get_transaction(tx_input['txid']) - asset_id = tx['transaction']['asset']['id'] + from bigchaindb.models import Transaction + + tx_create = b.get_owned_ids(user_vk).pop() + tx_create = b.get_transaction(tx_create.txid) + asset_id = tx_create.asset.data_id txs = b.get_txs_by_asset_id(asset_id) assert len(txs) == 1 - assert txs[0]['id'] == tx['id'] - assert txs[0]['transaction']['asset']['id'] == asset_id + assert txs[0].id == tx_create.id + assert txs[0].asset.data_id == asset_id # create a transfer transaction - tx_transfer = b.create_transaction(user_vk, user_vk, tx_input, 'TRANSFER') - tx_transfer_signed = b.sign_transaction(tx_transfer, user_sk) + tx_transfer = Transaction.transfer(tx_create.to_inputs(), [user_vk], tx_create.asset) + tx_transfer_signed = tx_transfer.sign([user_sk]) # create the block block = b.create_block([tx_transfer_signed]) b.write_block(block, durability='hard') # vote the block valid - vote = b.vote(block['id'], b.get_last_voted_block()['id'], True) + vote = b.vote(block.id, b.get_last_voted_block().id, True) b.write_vote(vote) txs = b.get_txs_by_asset_id(asset_id) assert len(txs) == 2 - assert tx['id'] in [t['id'] for t in txs] - assert tx_transfer['id'] in [t['id'] for t in txs] - assert asset_id == txs[0]['transaction']['asset']['id'] - assert asset_id == txs[1]['transaction']['asset']['id'] + assert tx_create.id in [t.id for t in txs] + assert tx_transfer.id in [t.id for t in txs] + assert asset_id == txs[0].asset.data_id + assert asset_id == txs[1].asset.data_id diff --git a/tests/db/test_bigchain_api.py b/tests/db/test_bigchain_api.py index b9f1417b..3d420350 100644 --- a/tests/db/test_bigchain_api.py +++ b/tests/db/test_bigchain_api.py @@ -577,9 +577,11 @@ class TestTransactionValidation(object): from bigchaindb.models import Transaction input_tx = b.get_owned_ids(user_vk).pop() + input_transaction = b.get_transaction(input_tx.txid) sk, vk = generate_key_pair() tx = Transaction.create([vk], [user_vk]) tx.operation = 'TRANSFER' + tx.asset = input_transaction.asset tx.fulfillments[0].tx_input = input_tx with pytest.raises(InvalidSignature):