From a212aba35b3409f8d874a3e70c6596fe6bbd3538 Mon Sep 17 00:00:00 2001 From: Rodolphe Marques Date: Sun, 6 Nov 2016 20:00:47 +0100 Subject: [PATCH] Added validation for amounts Added a new db call to return an asset instance given the id Created tests --- bigchaindb/core.py | 28 +++++++++++++++++- bigchaindb/models.py | 25 ++++++++++++++-- tests/assets/test_digital_assets.py | 27 +++++++++++++++++- tests/assets/test_divisible_assets.py | 41 +++++++++++++++++++++++++-- 4 files changed, 114 insertions(+), 7 deletions(-) diff --git a/bigchaindb/core.py b/bigchaindb/core.py index 64537b74..f422ced2 100644 --- a/bigchaindb/core.py +++ b/bigchaindb/core.py @@ -6,7 +6,7 @@ from time import time from itertools import compress from bigchaindb.common import crypto, exceptions from bigchaindb.common.util import gen_timestamp, serialize -from bigchaindb.common.transaction import TransactionLink, Metadata +from bigchaindb.common.transaction import TransactionLink, Metadata, Asset import rethinkdb as r @@ -366,6 +366,32 @@ class Bigchain(object): return [Transaction.from_dict(tx) for tx in cursor] + def get_asset_by_id(self, asset_id): + """Returns the asset associated with an asset_id + + Args: + asset_id (str): The asset id + + Returns: + :class:`~bigchaindb.common.transaction.Asset` if the asset + exists else None + """ + 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) + .filter(lambda transaction: + transaction['transaction']['operation'] == 'CREATE') + .pluck({'transaction': 'asset'})) + cursor = list(cursor) + + if cursor: + return Asset.from_dict(cursor[0]['transaction']['asset']) + + return 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 a7660582..7f993ed4 100644 --- a/bigchaindb/models.py +++ b/bigchaindb/models.py @@ -3,7 +3,7 @@ from bigchaindb.common.exceptions import (InvalidHash, InvalidSignature, OperationError, DoubleSpend, TransactionDoesNotExist, FulfillmentNotInValidBlock, - AssetIdMismatch) + AssetIdMismatch, AmountError) from bigchaindb.common.transaction import Transaction, Asset from bigchaindb.common.util import gen_timestamp, serialize @@ -41,7 +41,8 @@ class Transaction(Transaction): if inputs_defined: raise ValueError('A CREATE operation has no inputs') # validate asset - self.asset._validate_asset() + amount = sum([condition.amount for condition in self.conditions]) + self.asset._validate_asset(amount=amount) elif self.operation == Transaction.TRANSFER: if not inputs_defined: raise ValueError('Only `CREATE` transactions can have null ' @@ -49,6 +50,7 @@ class Transaction(Transaction): # check inputs # store the inputs so that we can check if the asset ids match input_txs = [] + input_amount = 0 for ffill in self.fulfillments: input_txid = ffill.tx_input.txid input_cid = ffill.tx_input.cid @@ -71,11 +73,28 @@ class Transaction(Transaction): input_conditions.append(input_tx.conditions[input_cid]) input_txs.append(input_tx) + input_amount += input_tx.conditions[input_cid].amount # 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') + raise AssetIdMismatch(('The asset id of the input does not' + ' match the asset id of the' + ' transaction')) + + # get the asset creation to see if its divisible or not + asset = bigchain.get_asset_by_id(asset_id) + # validate the asset + asset._validate_asset(amount=input_amount) + # validate the amounts + output_amount = sum([condition.amount for + condition in self.conditions]) + if output_amount != input_amount: + raise AmountError(('The amout used in the inputs `{}`' + ' needs to be same as the amount used' + ' in the outputs `{}`') + .format(input_amount, output_amount)) + 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 0bf1c7b8..46a4463a 100644 --- a/tests/assets/test_digital_assets.py +++ b/tests/assets/test_digital_assets.py @@ -146,7 +146,7 @@ def test_get_txs_by_asset_id(b, user_vk, user_sk): assert txs[0].asset.data_id == asset_id # create a transfer transaction - tx_transfer = Transaction.transfer(tx_create.to_inputs(), [user_vk], + tx_transfer = Transaction.transfer(tx_create.to_inputs(), [([user_vk], 1)], tx_create.asset) tx_transfer_signed = tx_transfer.sign([user_sk]) # create the block @@ -165,6 +165,31 @@ def test_get_txs_by_asset_id(b, user_vk, user_sk): assert asset_id == txs[1].asset.data_id +@pytest.mark.usefixtures('inputs') +def test_get_asset_by_id(b, user_vk, user_sk): + 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 + + # create a transfer transaction + tx_transfer = Transaction.transfer(tx_create.to_inputs(), [([user_vk], 1)], + 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) + b.write_vote(vote) + + txs = b.get_txs_by_asset_id(asset_id) + assert len(txs) == 2 + + asset = b.get_asset_by_id(asset_id) + assert asset == tx_create.asset + def test_create_invalid_divisible_asset(b, user_vk, user_sk): from bigchaindb.models import Transaction, Asset from bigchaindb.common.exceptions import AmountError diff --git a/tests/assets/test_divisible_assets.py b/tests/assets/test_divisible_assets.py index 40f61cba..b227569a 100644 --- a/tests/assets/test_divisible_assets.py +++ b/tests/assets/test_divisible_assets.py @@ -546,6 +546,45 @@ def test_multiple_in_different_transactions(b, user_vk, user_sk): assert fid1_input == tx_transfer1.id +# In a TRANSFER transaction of a divisible asset the amount being spent in the +# inputs needs to match the amount being sent in the outputs. +# In other words `amount_in_inputs - amount_in_outputs == 0` +@pytest.mark.usefixtures('inputs') +def test_amount_error_transfer(b, user_vk, user_sk): + from bigchaindb.models import Transaction + from bigchaindb.common.transaction import Asset + from bigchaindb.common.exceptions import AmountError + + # CREATE divisible asset + asset = Asset(divisible=True) + tx_create = Transaction.create([b.me], [([user_vk], 100)], asset=asset) + tx_create_signed = tx_create.sign([b.me_private]) + # create block + block = b.create_block([tx_create_signed]) + assert block.validate(b) == block + b.write_block(block, durability='hard') + # vote + vote = b.vote(block.id, b.get_last_voted_block().id, True) + b.write_vote(vote) + + # TRANSFER + # output amount less than input amount + tx_transfer = Transaction.transfer(tx_create.to_inputs(), [([b.me], 50)], + asset=tx_create.asset) + tx_transfer_signed = tx_transfer.sign([user_sk]) + with pytest.raises(AmountError): + tx_transfer_signed.validate(b) + + # TRANSFER + # output amount greater than input amount + tx_transfer = Transaction.transfer(tx_create.to_inputs(), [([b.me], 101)], + asset=tx_create.asset) + tx_transfer_signed = tx_transfer.sign([user_sk]) + with pytest.raises(AmountError): + tx_transfer_signed.validate(b) + + +@pytest.mark.skip @pytest.mark.usefixtures('inputs') def test_transaction_unfulfilled_fulfillments(b, user_vk, user_sk): @@ -576,8 +615,6 @@ def test_transaction_unfulfilled_fulfillments(b, user_vk, # invalid. Somehow the validation passes assert b.is_valid_transaction(tx_transfer_signed) == False -#test input output amount mismatch. Both when output is less and greater then input - @pytest.mark.skip(reason=('get_subcondition_from_vk does not always work' ' as expected'))