diff --git a/bigchaindb/common/transaction.py b/bigchaindb/common/transaction.py index 41d8a30c..ee8b58f3 100644 --- a/bigchaindb/common/transaction.py +++ b/bigchaindb/common/transaction.py @@ -9,7 +9,8 @@ from cryptoconditions.exceptions import ParsingError from bigchaindb.common.crypto import SigningKey, hash_data from bigchaindb.common.exceptions import (KeypairMismatchException, - InvalidHash, InvalidSignature) + InvalidHash, InvalidSignature, + AmountError) from bigchaindb.common.util import serialize, gen_timestamp @@ -268,7 +269,7 @@ class Condition(object): return cond @classmethod - def generate(cls, owners_after): + def generate(cls, owners_after, amount=1): """Generates a Condition from a specifically formed tuple or list. Note: @@ -305,7 +306,9 @@ class Condition(object): owners_after, threshold = owners_after else: threshold = len(owners_after) - + + if not isinstance(amount, int): + raise TypeError('`amount` must be a int') if not isinstance(owners_after, list): raise TypeError('`owners_after` must be an instance of list') if len(owners_after) == 0: @@ -316,12 +319,12 @@ class Condition(object): ffill = Ed25519Fulfillment(public_key=owners_after[0]) except TypeError: ffill = owners_after[0] - return cls(ffill, owners_after) + return cls(ffill, owners_after, amount=amount) else: initial_cond = ThresholdSha256Fulfillment(threshold=threshold) threshold_cond = reduce(cls._gen_condition, owners_after, initial_cond) - return cls(threshold_cond, owners_after) + return cls(threshold_cond, owners_afteri, amount=amount) @classmethod def _gen_condition(cls, initial, current): @@ -466,7 +469,7 @@ class Asset(object): """Generates a unqiue uuid for an Asset""" return str(uuid4()) - def _validate_asset(self): + def _validate_asset(self, amount=None): """Validates the asset""" if self.data is not None and not isinstance(self.data, dict): raise TypeError('`data` must be a dict instance or None') @@ -477,6 +480,29 @@ class Asset(object): if not isinstance(self.updatable, bool): raise TypeError('`updatable` must be a boolean') + if self.refillable: + raise NotImplementedError('Refillable assets are not yet' + ' implemented') + if self.updatable: + raise NotImplementedError('Updatable assets are not yet' + ' implemented') + + # If the amount is supplied we can perform extra validations to + # the asset + if amount is not None: + if not isinstance(amount, int): + raise TypeError('`amount` must be an int') + + if self.divisible is False and amount != 1: + raise AmountError('non divisible assets always have' + ' amount equal to one') + + # Since refillable assets are not yet implemented this should + # raise and exception + if self.divisible is True and amount < 2: + raise AmountError('divisible assets must have an amount' + ' greater than one') + class Metadata(object): """Metadata is used to store a dictionary and its hash in a Transaction.""" @@ -621,6 +647,7 @@ class Transaction(object): if conditions is not None and not isinstance(conditions, list): raise TypeError('`conditions` must be a list instance or None') + # TODO: Check if there is a case in which conditions may be None elif conditions is None: self.conditions = [] else: @@ -628,6 +655,7 @@ class Transaction(object): if fulfillments is not None and not isinstance(fulfillments, list): raise TypeError('`fulfillments` must be a list instance or None') + # TODO: Check if there is a case in which fulfillments may be None elif fulfillments is None: self.fulfillments = [] else: @@ -638,9 +666,16 @@ class Transaction(object): else: self.metadata = metadata + # validate asset + # we know that each transaction relates to a single asset + # we can sum the amount of all the conditions + amount = sum([condition.amount for condition in self.conditions]) + self.asset._validate_asset(amount=amount) + + @classmethod def create(cls, owners_before, owners_after, metadata=None, asset=None, - secret=None, time_expire=None): + secret=None, time_expire=None, amount=1): """A simple way to generate a `CREATE` transaction. Note: @@ -675,6 +710,8 @@ class Transaction(object): raise TypeError('`owners_before` must be a list instance') if not isinstance(owners_after, list): raise TypeError('`owners_after` must be a list instance') + if not isinstance(amount, int): + raise TypeError('`amount` must be a int') metadata = Metadata(metadata) if len(owners_before) == len(owners_after) and len(owners_after) == 1: @@ -683,7 +720,7 @@ class Transaction(object): # fulfillment for the fulfillment and condition. ffill = Ed25519Fulfillment(public_key=owners_before[0]) ffill_tx = Fulfillment(ffill, owners_before) - cond_tx = Condition.generate(owners_after) + cond_tx = Condition.generate(owners_after, amount=amount) return cls(cls.CREATE, asset, [ffill_tx], [cond_tx], metadata) elif len(owners_before) == len(owners_after) and len(owners_after) > 1: @@ -693,7 +730,8 @@ class Transaction(object): ffills = [Fulfillment(Ed25519Fulfillment(public_key=owner_before), [owner_before]) for owner_before in owners_before] - conds = [Condition.generate(owners) for owners in owners_after] + conds = [Condition.generate(owners, amount=amount) + for owners in owners_after] return cls(cls.CREATE, asset, ffills, conds, metadata) elif len(owners_before) == 1 and len(owners_after) > 1: @@ -707,7 +745,7 @@ class Transaction(object): secret is not None): # NOTE: Hashlock condition case hashlock = PreimageSha256Fulfillment(preimage=secret) - cond_tx = Condition(hashlock.condition_uri) + cond_tx = Condition(hashlock.condition_uri, amount=amount) ffill = Ed25519Fulfillment(public_key=owners_before[0]) ffill_tx = Fulfillment(ffill, owners_before) return cls(cls.CREATE, asset, [ffill_tx], [cond_tx], metadata) diff --git a/bigchaindb/core.py b/bigchaindb/core.py index 83b85652..acdecd28 100644 --- a/bigchaindb/core.py +++ b/bigchaindb/core.py @@ -183,7 +183,7 @@ class Bigchain(object): except (ValueError, exceptions.OperationError, exceptions.TransactionDoesNotExist, exceptions.TransactionOwnerError, exceptions.DoubleSpend, exceptions.InvalidHash, exceptions.InvalidSignature, - exceptions.FulfillmentNotInValidBlock): + exceptions.FulfillmentNotInValidBlock, exceptions.AmountError): return False def get_transaction(self, txid, include_status=False): diff --git a/tests/assets/test_digital_assets.py b/tests/assets/test_digital_assets.py index e18684c5..5d4fb5d1 100644 --- a/tests/assets/test_digital_assets.py +++ b/tests/assets/test_digital_assets.py @@ -1,4 +1,6 @@ import pytest +from unittest.mock import patch + from ..db.conftest import inputs @@ -161,3 +163,47 @@ def test_get_txs_by_asset_id(b, user_vk, user_sk): 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 + + +def test_create_invalid_divisible_asset(b, user_vk, user_sk): + from bigchaindb.models import Transaction, Asset + from bigchaindb.common.exceptions import AmountError + + # non divisible assets cannot have amount > 1 + # Transaction.__init__ should raise an exception + asset = Asset(divisible=False) + with pytest.raises(AmountError): + Transaction.create([user_vk], [user_vk], asset=asset, amount=2) + + # divisible assets need to have an amount > 1 + # Transaction.__init__ should raise an exception + asset = Asset(divisible=True) + with pytest.raises(AmountError): + Transaction.create([user_vk], [user_vk], asset=asset, amount=1) + + # even if a transaction is badly constructed the server should raise the + # exception + asset = Asset(divisible=False) + with patch.object(Asset, '_validate_asset', return_value=None): + tx = Transaction.create([user_vk], [user_vk], asset=asset, amount=2) + tx_signed = tx.sign([user_sk]) + with pytest.raises(AmountError): + tx_signed.validate(b) + assert b.is_valid_transaction(tx_signed) is False + + asset = Asset(divisible=True) + with patch.object(Asset, '_validate_asset', return_value=None): + tx = Transaction.create([user_vk], [user_vk], asset=asset, amount=1) + tx_signed = tx.sign([user_sk]) + with pytest.raises(AmountError): + tx_signed.validate(b) + assert b.is_valid_transaction(tx_signed) is False + + +def test_create_valid_divisible_asset(b, user_vk, user_sk): + from bigchaindb.models import Transaction, Asset + + asset = Asset(divisible=True) + tx = Transaction.create([user_vk], [user_vk], asset=asset, amount=2) + tx_signed = tx.sign([user_sk]) + assert b.is_valid_transaction(tx_signed) diff --git a/tests/common/test_asset.py b/tests/common/test_asset.py index edfbcb5f..cddaae64 100644 --- a/tests/common/test_asset.py +++ b/tests/common/test_asset.py @@ -22,6 +22,7 @@ def test_asset_creation_with_data(data): def test_asset_invalid_asset_initialization(): from bigchaindb.common.transaction import Asset + # check types with raises(TypeError): Asset(data='some wrong type') with raises(TypeError): @@ -31,6 +32,12 @@ def test_asset_invalid_asset_initialization(): with raises(TypeError): Asset(updatable=1) + # check for features that are not yet implemented + with raises(NotImplementedError): + Asset(updatable=True) + with raises(NotImplementedError): + Asset(refillable=True) + def test_invalid_asset_comparison(data, data_id): from bigchaindb.common.transaction import Asset @@ -69,12 +76,17 @@ def test_asset_deserialization(data, data_id): def test_validate_asset(): from bigchaindb.common.transaction import Asset + from bigchaindb.common.exceptions import AmountError + # test amount errors + asset = Asset(divisible=False) + with raises(AmountError): + asset._validate_asset(amount=2) + + asset = Asset(divisible=True) + with raises(AmountError): + asset._validate_asset(amount=1) + + asset = Asset() with raises(TypeError): - Asset(divisible=1) - with raises(TypeError): - Asset(refillable=1) - with raises(TypeError): - Asset(updatable=1) - with raises(TypeError): - Asset(data='we need more lemon pledge') + asset._validate_asset(amount='a')