From 2d6dfaa6f448cb96aaeb200203174eeeb067db1c Mon Sep 17 00:00:00 2001 From: troymc Date: Sat, 29 Oct 2016 16:35:15 +0200 Subject: [PATCH 01/54] Rename FulfillmentNotInValidBlock to TransactionNotInValidBlock --- bigchaindb/common/exceptions.py | 6 +++--- bigchaindb/core.py | 2 +- bigchaindb/models.py | 6 ++++-- tests/db/test_bigchain_api.py | 6 +++--- 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/bigchaindb/common/exceptions.py b/bigchaindb/common/exceptions.py index 2e1dc670..72e300fd 100644 --- a/bigchaindb/common/exceptions.py +++ b/bigchaindb/common/exceptions.py @@ -69,9 +69,9 @@ class CyclicBlockchainError(Exception): """Raised when there is a cycle in the blockchain""" -class FulfillmentNotInValidBlock(Exception): - """Raised when a transaction depends on an invalid or undecided - fulfillment""" +class TransactionNotInValidBlock(Exception): + """Raised when a transfer transaction is attempting to fulfill the + conditions of a transaction that is in an invalid or undecided block""" class AssetIdMismatch(Exception): diff --git a/bigchaindb/core.py b/bigchaindb/core.py index 83b85652..a5ec25e0 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.TransactionNotInValidBlock): return False def get_transaction(self, txid, include_status=False): diff --git a/bigchaindb/models.py b/bigchaindb/models.py index 1334b2de..87ea57bb 100644 --- a/bigchaindb/models.py +++ b/bigchaindb/models.py @@ -2,7 +2,7 @@ from bigchaindb.common.crypto import hash_data, VerifyingKey, SigningKey from bigchaindb.common.exceptions import (InvalidHash, InvalidSignature, OperationError, DoubleSpend, TransactionDoesNotExist, - FulfillmentNotInValidBlock, + TransactionNotInValidBlock, AssetIdMismatch) from bigchaindb.common.transaction import Transaction, Asset from bigchaindb.common.util import gen_timestamp, serialize @@ -54,6 +54,8 @@ class Transaction(Transaction): OperationError: if the transaction operation is not supported TransactionDoesNotExist: if the input of the transaction is not found + TransactionNotInValidBlock: if the input of the transaction is not + in a valid block TransactionOwnerError: if the new transaction is using an input it doesn't own DoubleSpend: if the transaction is a double spend @@ -90,7 +92,7 @@ class Transaction(Transaction): .format(input_txid)) if status != bigchain.TX_VALID: - raise FulfillmentNotInValidBlock( + raise TransactionNotInValidBlock( 'input `{}` does not exist in a valid block'.format( input_txid)) diff --git a/tests/db/test_bigchain_api.py b/tests/db/test_bigchain_api.py index f0a88c44..b52dd3a7 100644 --- a/tests/db/test_bigchain_api.py +++ b/tests/db/test_bigchain_api.py @@ -650,9 +650,9 @@ class TestTransactionValidation(object): assert transfer_tx == b.validate_transaction(transfer_tx) @pytest.mark.usefixtures('inputs') - def test_fulfillment_not_in_valid_block(self, b, user_vk, user_sk): + def test_transaction_not_in_valid_block(self, b, user_vk, user_sk): from bigchaindb.models import Transaction - from bigchaindb.common.exceptions import FulfillmentNotInValidBlock + from bigchaindb.common.exceptions import TransactionNotInValidBlock input_tx = b.get_owned_ids(user_vk).pop() input_tx = b.get_transaction(input_tx.txid) @@ -673,7 +673,7 @@ class TestTransactionValidation(object): transfer_tx.asset) tx_invalid = tx_invalid.sign([user_sk]) - with pytest.raises(FulfillmentNotInValidBlock): + with pytest.raises(TransactionNotInValidBlock): b.validate_transaction(tx_invalid) From dd382ee4e64adfc80db345e5e721b2a2c1a32bf6 Mon Sep 17 00:00:00 2001 From: Rodolphe Marques Date: Thu, 3 Nov 2016 15:57:05 +0100 Subject: [PATCH 02/54] Added ability to `CREATE` divisible assets --- bigchaindb/common/transaction.py | 58 ++++++++++++++++++++++++----- bigchaindb/core.py | 2 +- tests/assets/test_digital_assets.py | 46 +++++++++++++++++++++++ tests/common/test_asset.py | 26 +++++++++---- 4 files changed, 114 insertions(+), 18 deletions(-) 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') From 63f5879cb2a679055257506892dcc87976e92c9b Mon Sep 17 00:00:00 2001 From: Rodolphe Marques Date: Fri, 4 Nov 2016 11:31:07 +0100 Subject: [PATCH 03/54] consolidate Asset model in common --- bigchaindb/common/transaction.py | 34 ++++++++++++++++++++++++++--- bigchaindb/models.py | 32 +-------------------------- tests/assets/test_digital_assets.py | 6 ++--- 3 files changed, 35 insertions(+), 37 deletions(-) diff --git a/bigchaindb/common/transaction.py b/bigchaindb/common/transaction.py index ee8b58f3..feaa2203 100644 --- a/bigchaindb/common/transaction.py +++ b/bigchaindb/common/transaction.py @@ -10,7 +10,7 @@ from cryptoconditions.exceptions import ParsingError from bigchaindb.common.crypto import SigningKey, hash_data from bigchaindb.common.exceptions import (KeypairMismatchException, InvalidHash, InvalidSignature, - AmountError) + AmountError, AssetIdMismatch) from bigchaindb.common.util import serialize, gen_timestamp @@ -306,7 +306,7 @@ 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): @@ -324,7 +324,7 @@ class Condition(object): initial_cond = ThresholdSha256Fulfillment(threshold=threshold) threshold_cond = reduce(cls._gen_condition, owners_after, initial_cond) - return cls(threshold_cond, owners_afteri, amount=amount) + return cls(threshold_cond, owners_after, amount=amount) @classmethod def _gen_condition(cls, initial, current): @@ -469,6 +469,34 @@ class Asset(object): """Generates a unqiue uuid for an Asset""" return str(uuid4()) + @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() + def _validate_asset(self, amount=None): """Validates the asset""" if self.data is not None and not isinstance(self.data, dict): diff --git a/bigchaindb/models.py b/bigchaindb/models.py index 1334b2de..a7660582 100644 --- a/bigchaindb/models.py +++ b/bigchaindb/models.py @@ -8,36 +8,6 @@ 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. @@ -190,7 +160,7 @@ class Block(object): def is_signature_valid(self): block = self.to_dict()['block'] - # cc only accepts bytesting messages + # cc only accepts bytesting messages block_serialized = serialize(block).encode() verifying_key = VerifyingKey(block['node_pubkey']) try: diff --git a/tests/assets/test_digital_assets.py b/tests/assets/test_digital_assets.py index 5d4fb5d1..0bf1c7b8 100644 --- a/tests/assets/test_digital_assets.py +++ b/tests/assets/test_digital_assets.py @@ -1,7 +1,7 @@ import pytest from unittest.mock import patch -from ..db.conftest import inputs +from ..db.conftest import inputs # noqa @pytest.mark.usefixtures('inputs') @@ -171,7 +171,7 @@ def test_create_invalid_divisible_asset(b, user_vk, user_sk): # non divisible assets cannot have amount > 1 # Transaction.__init__ should raise an exception - asset = Asset(divisible=False) + asset = Asset(divisible=False) with pytest.raises(AmountError): Transaction.create([user_vk], [user_vk], asset=asset, amount=2) @@ -202,7 +202,7 @@ def test_create_invalid_divisible_asset(b, user_vk, user_sk): 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]) From 48084ec47a65657d082c165bc7d6032516e3af99 Mon Sep 17 00:00:00 2001 From: Rodolphe Marques Date: Fri, 4 Nov 2016 15:34:39 +0100 Subject: [PATCH 04/54] multiple outputs in create transaction --- bigchaindb/common/transaction.py | 7 ++----- tests/common/test_transaction.py | 4 ++-- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/bigchaindb/common/transaction.py b/bigchaindb/common/transaction.py index feaa2203..fb12b5b6 100644 --- a/bigchaindb/common/transaction.py +++ b/bigchaindb/common/transaction.py @@ -752,19 +752,16 @@ class Transaction(object): return cls(cls.CREATE, asset, [ffill_tx], [cond_tx], metadata) elif len(owners_before) == len(owners_after) and len(owners_after) > 1: - raise NotImplementedError('Multiple inputs and outputs not' - 'available for CREATE') - # NOTE: Multiple inputs and outputs case. Currently not supported. ffills = [Fulfillment(Ed25519Fulfillment(public_key=owner_before), [owner_before]) for owner_before in owners_before] - conds = [Condition.generate(owners, amount=amount) + 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: # NOTE: Multiple owners case - cond_tx = Condition.generate(owners_after) + cond_tx = Condition.generate(owners_after, 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/tests/common/test_transaction.py b/tests/common/test_transaction.py index 5f2d58fb..04bd7eb5 100644 --- a/tests/common/test_transaction.py +++ b/tests/common/test_transaction.py @@ -816,14 +816,14 @@ def test_create_create_transaction_multiple_io(user_cond, user2_cond, user_pub, assert tx == expected -@mark.skip(reason='Multiple inputs and outputs in CREATE not supported') +# @mark.skip(reason='Multiple inputs and outputs in CREATE not supported') # TODO: Add digital assets def test_validate_multiple_io_create_transaction(user_pub, user_priv, user2_pub, user2_priv): from bigchaindb.common.transaction import Transaction tx = Transaction.create([user_pub, user2_pub], [user_pub, user2_pub], - {'message': 'hello'}) + metadata={'message': 'hello'}) tx = tx.sign([user_priv, user2_priv]) assert tx.fulfillments_valid() is True From 5b5c701e0adff938fb35c685705eaa7f08bb04ff Mon Sep 17 00:00:00 2001 From: Rodolphe Marques Date: Sun, 6 Nov 2016 00:04:27 +0100 Subject: [PATCH 05/54] Finished implementing divisible assets for CREATE transactions Simplified Transaction.create logic Created tests --- bigchaindb/common/transaction.py | 51 +++++++++--- tests/assets/test_divisible_assets.py | 109 ++++++++++++++++++++++++++ 2 files changed, 147 insertions(+), 13 deletions(-) create mode 100644 tests/assets/test_divisible_assets.py diff --git a/bigchaindb/common/transaction.py b/bigchaindb/common/transaction.py index fb12b5b6..468ee649 100644 --- a/bigchaindb/common/transaction.py +++ b/bigchaindb/common/transaction.py @@ -99,6 +99,14 @@ class Fulfillment(object): ffill['fid'] = fid return ffill + @classmethod + def generate(cls, owners_before): + # TODO: write docstring + + if len(owners_before) == 1: + ffill = Ed25519Fulfillment(public_key=owners_before[0]) + return cls(ffill, owners_before) + @classmethod def from_dict(cls, ffill): """Transforms a Python dictionary to a Fulfillment object. @@ -269,7 +277,8 @@ class Condition(object): return cond @classmethod - def generate(cls, owners_after, amount=1): + def generate(cls, owners_after, amount): + # TODO: Update docstring """Generates a Condition from a specifically formed tuple or list. Note: @@ -703,7 +712,8 @@ class Transaction(object): @classmethod def create(cls, owners_before, owners_after, metadata=None, asset=None, - secret=None, time_expire=None, amount=1): + secret=None, time_expire=None): + # TODO: Update docstring """A simple way to generate a `CREATE` transaction. Note: @@ -738,10 +748,24 @@ 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) + + ffils = [] + conds = [] + + # generate_conditions + for owner_after in owners_after: + pub_keys, amount = owner_after + conds.append(Condition.generate(pub_keys, amount)) + + # generate fulfillments + ffils.append(Fulfillment.generate(owners_before)) + + return cls(cls.CREATE, asset, ffils, conds, metadata) + + + if len(owners_before) == len(owners_after) and len(owners_after) == 1: # NOTE: Standard case, one owner before, one after. # NOTE: For this case its sufficient to use the same @@ -755,7 +779,7 @@ class Transaction(object): ffills = [Fulfillment(Ed25519Fulfillment(public_key=owner_before), [owner_before]) for owner_before in owners_before] - conds = [Condition.generate([owners], amount=amount) + conds = [Condition.generate([owners], amount) for owners in owners_after] return cls(cls.CREATE, asset, ffills, conds, metadata) @@ -1138,14 +1162,15 @@ class Transaction(object): tx_serialized, input_condition_uri) - if not fulfillments_count == conditions_count == \ - input_condition_uris_count: - raise ValueError('Fulfillments, conditions and ' - 'input_condition_uris must have the same count') - else: - partial_transactions = map(gen_tx, self.fulfillments, - self.conditions, input_condition_uris) - return all(partial_transactions) + # TODO: Why?? Need to ask @TimDaub + # if not fulfillments_count == conditions_count == \ + # input_condition_uris_count: + # raise ValueError('Fulfillments, conditions and ' + # 'input_condition_uris must have the same count') + # else: + partial_transactions = map(gen_tx, self.fulfillments, + self.conditions, input_condition_uris) + return all(partial_transactions) @staticmethod def _fulfillment_valid(fulfillment, operation, tx_serialized, diff --git a/tests/assets/test_divisible_assets.py b/tests/assets/test_divisible_assets.py new file mode 100644 index 00000000..a52c4e19 --- /dev/null +++ b/tests/assets/test_divisible_assets.py @@ -0,0 +1,109 @@ +import pytest + + +# CREATE divisible asset +# Single input +# Single owners_before +# Single output +# single owners_after +def test_single_in_single_own_single_out_single_own_create(b, user_vk): + from bigchaindb.models import Transaction + from bigchaindb.common.transaction import Asset + + asset = Asset(divisible=True) + tx = Transaction.create([b.me], [([user_vk], 100)], asset=asset) + tx_signed = tx.sign([b.me_private]) + + assert tx_signed.validate(b) == tx_signed + assert len(tx_signed.conditions) == 1 + assert tx_signed.conditions[0].amount == 100 + assert len(tx_signed.fulfillments) == 1 + + +# CREATE divisible asset +# Single input +# Single onwers_before +# Multiple outputs +# Single owners_after per output +def test_single_in_single_own_multiple_out_single_own_create(b, user_vk): + from bigchaindb.models import Transaction + from bigchaindb.common.transaction import Asset + + asset = Asset(divisible=True) + tx = Transaction.create([b.me], [([user_vk], 50), ([user_vk], 50)], asset=asset) + tx_signed = tx.sign([b.me_private]) + + assert tx_signed.validate(b) == tx_signed + assert len(tx_signed.conditions) == 2 + assert tx_signed.conditions[0].amount == 50 + assert tx_signed.conditions[1].amount == 50 + assert len(tx_signed.fulfillments) == 1 + + +# CREATE divisible asset +# Single input +# Single owners_before +# Single output +# Multiple owners_after +def test_single_in_single_own_single_out_multiple_own_create(b, user_vk): + from bigchaindb.models import Transaction + from bigchaindb.common.transaction import Asset + + asset = Asset(divisible=True) + tx = Transaction.create([b.me], [([user_vk, user_vk], 100)], asset=asset) + tx_signed = tx.sign([b.me_private]) + + assert tx_signed.validate(b) == tx_signed + assert len(tx_signed.conditions) == 1 + assert tx_signed.conditions[0].amount == 100 + + condition = tx_signed.conditions[0].to_dict() + assert 'subfulfillments' in condition['condition']['details'] + assert len(condition['condition']['details']['subfulfillments']) == 2 + + assert len(tx_signed.fulfillments) == 1 + + +# CREATE divisible asset +# Single input +# Single owners_before +# Multiple outputs +# Mix: one output with a single owners_after, one output with multiple +# owners_after +def test_single_in_single_own_multiple_out_mix_own_create(b, user_vk): + from bigchaindb.models import Transaction + from bigchaindb.common.transaction import Asset + + asset = Asset(divisible=True) + tx = Transaction.create([b.me], + [([user_vk], 50), ([user_vk, user_vk], 50)], + asset=asset) + tx_signed = tx.sign([b.me_private]) + + assert tx_signed.validate(b) == tx_signed + assert len(tx_signed.conditions) == 2 + assert tx_signed.conditions[0].amount == 50 + assert tx_signed.conditions[1].amount == 50 + + condition_cid1 = tx_signed.conditions[1].to_dict() + assert 'subfulfillments' in condition_cid1['condition']['details'] + assert len(condition_cid1['condition']['details']['subfulfillments']) == 2 + + assert len(tx_signed.fulfillments) == 1 + + +# CREATE divisible asset +# Single input +# Multiple owners_before +# Ouput combinations already tested above +# TODO: Support multiple owners_before in CREATE transactions +@pytest.mark.skip(reason=('CREATE transaction do not support multiple' + ' owners_before')) +def test_single_in_multiple_own_single_out_single_own_create(b, user_vk): + from bigchaindb.models import Transaction + from bigchaindb.common.transaction import Asset + + asset = Asset(divisible=True) + tx = Transaction.create([b.me, b.me], [([user_vk], 100)], asset=asset) + tx_signed = tx.sign([b.me, b.me]) + assert tx_signed.validate(b) == tx_signed From ee3b96718488409af5ce8970db161a6d5171d150 Mon Sep 17 00:00:00 2001 From: Rodolphe Marques Date: Sun, 6 Nov 2016 01:55:47 +0100 Subject: [PATCH 06/54] Added support for divisible assets in TRANSFER transactions Created tests --- bigchaindb/common/transaction.py | 31 +++-- bigchaindb/core.py | 2 +- tests/assets/test_divisible_assets.py | 164 +++++++++++++++++++++++++- tests/db/conftest.py | 2 +- 4 files changed, 184 insertions(+), 15 deletions(-) diff --git a/bigchaindb/common/transaction.py b/bigchaindb/common/transaction.py index 468ee649..2ee7a062 100644 --- a/bigchaindb/common/transaction.py +++ b/bigchaindb/common/transaction.py @@ -779,7 +779,7 @@ class Transaction(object): ffills = [Fulfillment(Ed25519Fulfillment(public_key=owner_before), [owner_before]) for owner_before in owners_before] - conds = [Condition.generate([owners], amount) + conds = [Condition.generate([owners], amount) for owners in owners_after] return cls(cls.CREATE, asset, ffills, conds, metadata) @@ -857,20 +857,27 @@ class Transaction(object): if not isinstance(owners_after, list): raise TypeError('`owners_after` must be a list instance') - # NOTE: See doc strings `Note` for description. - if len(inputs) == len(owners_after): - if len(owners_after) == 1: - conditions = [Condition.generate(owners_after)] - elif len(owners_after) > 1: - conditions = [Condition.generate(owners) for owners - in owners_after] - else: - raise ValueError("`inputs` and `owners_after`'s count must be the " - "same") + # # NOTE: See doc strings `Note` for description. + # if len(inputs) == len(owners_after): + # if len(owners_after) == 1: + # conditions = [Condition.generate(owners_after)] + # elif len(owners_after) > 1: + # conditions = [Condition.generate(owners) for owners + # in owners_after] + # else: + # # TODO: Why?? + # raise ValueError("`inputs` and `owners_after`'s count must be the " + # "same") + + conds = [] + for owner_after in owners_after: + pub_keys, amount = owner_after + conds.append(Condition.generate(pub_keys, amount)) + metadata = Metadata(metadata) inputs = deepcopy(inputs) - return cls(cls.TRANSFER, asset, inputs, conditions, metadata) + return cls(cls.TRANSFER, asset, inputs, conds, metadata) def __eq__(self, other): try: diff --git a/bigchaindb/core.py b/bigchaindb/core.py index acdecd28..64537b74 100644 --- a/bigchaindb/core.py +++ b/bigchaindb/core.py @@ -551,7 +551,7 @@ class Bigchain(object): """Prepare a genesis block.""" metadata = {'message': 'Hello World from the BigchainDB'} - transaction = Transaction.create([self.me], [self.me], + transaction = Transaction.create([self.me], [([self.me], 1)], metadata=metadata) # NOTE: The transaction model doesn't expose an API to generate a diff --git a/tests/assets/test_divisible_assets.py b/tests/assets/test_divisible_assets.py index a52c4e19..bb5ed5ec 100644 --- a/tests/assets/test_divisible_assets.py +++ b/tests/assets/test_divisible_assets.py @@ -1,11 +1,13 @@ import pytest +from ..db.conftest import inputs # noqa + # CREATE divisible asset # Single input # Single owners_before # Single output -# single owners_after +# Single owners_after def test_single_in_single_own_single_out_single_own_create(b, user_vk): from bigchaindb.models import Transaction from bigchaindb.common.transaction import Asset @@ -107,3 +109,163 @@ def test_single_in_multiple_own_single_out_single_own_create(b, user_vk): tx = Transaction.create([b.me, b.me], [([user_vk], 100)], asset=asset) tx_signed = tx.sign([b.me, b.me]) assert tx_signed.validate(b) == tx_signed + + +# TRANSFER divisible asset +# Single input +# Single owners_before +# Single output +# Single owners_after +# TODO: I don't really need inputs. But I need the database to be setup or +# else there will be no genesis block and b.get_last_voted_block will +# fail. +# Is there a better way of doing this? +@pytest.mark.usefixtures('inputs') +def test_single_in_single_own_single_out_single_own_transfer(b, user_vk, + user_sk): + from bigchaindb.models import Transaction + from bigchaindb.common.transaction import Asset + + # 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 + tx_transfer = Transaction.transfer(tx_create.to_inputs(), [([b.me], 100)], + asset=tx_create.asset) + tx_transfer_signed = tx_transfer.sign([user_sk]) + + assert tx_transfer_signed.validate(b) + assert len(tx_transfer_signed.conditions) == 1 + assert tx_transfer_signed.conditions[0].amount == 100 + assert len(tx_transfer_signed.fulfillments) == 1 + + +# TRANSFER divisible asset +# Single input +# Single owners_before +# Multiple output +# Single owners_after +@pytest.mark.usefixtures('inputs') +def test_single_in_single_own_multiple_out_single_own_transfer(b, user_vk, + user_sk): + from bigchaindb.models import Transaction + from bigchaindb.common.transaction import Asset + + # 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 + tx_transfer = Transaction.transfer(tx_create.to_inputs(), + [([b.me], 50), ([b.me], 50)], + asset=tx_create.asset) + tx_transfer_signed = tx_transfer.sign([user_sk]) + + assert tx_transfer_signed.validate(b) == tx_transfer_signed + assert len(tx_transfer_signed.conditions) == 2 + assert tx_transfer_signed.conditions[0].amount == 50 + assert tx_transfer_signed.conditions[1].amount == 50 + assert len(tx_transfer_signed.fulfillments) == 1 + + +# TRANSFER divisible asset +# Single input +# Single owners_before +# Single output +# Multiple owners_after +@pytest.mark.usefixtures('inputs') +def test_single_in_single_own_single_out_multiple_own_transfer(b, user_vk, + user_sk): + from bigchaindb.models import Transaction + from bigchaindb.common.transaction import Asset + + # 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 + tx_transfer = Transaction.transfer(tx_create.to_inputs(), + [([b.me, b.me], 100)], + asset=tx_create.asset) + tx_transfer_signed = tx_transfer.sign([user_sk]) + + assert tx_transfer_signed.validate(b) == tx_transfer_signed + assert len(tx_transfer_signed.conditions) == 1 + assert tx_transfer_signed.conditions[0].amount == 100 + + condition = tx_transfer_signed.conditions[0].to_dict() + assert 'subfulfillments' in condition['condition']['details'] + assert len(condition['condition']['details']['subfulfillments']) == 2 + + assert len(tx_transfer_signed.fulfillments) == 1 + + +# TRANSFER divisible asset +# Single input +# Single owners_before +# Multiple outputs +# Mix: one output with a single owners_after, one output with multiple +# owners_after +@pytest.mark.usefixtures('inputs') +def test_single_in_single_own_multiple_out_mix_own_transfer(b, user_vk, + user_sk): + from bigchaindb.models import Transaction + from bigchaindb.common.transaction import Asset + + # 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 + tx_transfer = Transaction.transfer(tx_create.to_inputs(), + [([b.me], 50), ([b.me, b.me], 50)], + asset=tx_create.asset) + tx_transfer_signed = tx_transfer.sign([user_sk]) + + assert tx_transfer_signed.validate(b) == tx_transfer_signed + assert len(tx_transfer_signed.conditions) == 2 + assert tx_transfer_signed.conditions[0].amount == 50 + assert tx_transfer_signed.conditions[1].amount == 50 + + condition_cid1 = tx_transfer_signed.conditions[1].to_dict() + assert 'subfulfillments' in condition_cid1['condition']['details'] + assert len(condition_cid1['condition']['details']['subfulfillments']) == 2 + + assert len(tx_transfer_signed.fulfillments) == 1 + + +#def test_single_in_multiple_own_single_out_single_own_create(b, user_vk): +#test input output amount mismatch. Both when output is less and greater then input diff --git a/tests/db/conftest.py b/tests/db/conftest.py index f55a4c34..d71ddb67 100644 --- a/tests/db/conftest.py +++ b/tests/db/conftest.py @@ -119,7 +119,7 @@ def inputs(user_vk): prev_block_id = g.id for block in range(4): transactions = [ - Transaction.create([b.me], [user_vk]).sign([b.me_private]) + Transaction.create([b.me], [([user_vk], 1)]).sign([b.me_private]) for i in range(10) ] block = b.create_block(transactions) From db55aa81538abeba5b75d9f4a6866396815b9cff Mon Sep 17 00:00:00 2001 From: Rodolphe Marques Date: Sun, 6 Nov 2016 18:09:43 +0100 Subject: [PATCH 07/54] Support for multiple io in TRANSFER transactions Create tests --- bigchaindb/common/transaction.py | 31 ++- tests/assets/test_divisible_assets.py | 351 +++++++++++++++++++++++++- 2 files changed, 371 insertions(+), 11 deletions(-) diff --git a/bigchaindb/common/transaction.py b/bigchaindb/common/transaction.py index 2ee7a062..7f18d1c4 100644 --- a/bigchaindb/common/transaction.py +++ b/bigchaindb/common/transaction.py @@ -469,6 +469,12 @@ class Asset(object): Returns: :class:`~bigchaindb.common.transaction.Asset` """ + # TODO: This is not correct. If using Transaction.from_dict() from a + # TRANSFER transaction we only have information about the `id`, meaning + # that even if its a divisible asset, since the key does not exist if will be + # set to False by default. + # Maybe use something like an AssetLink similar to TransactionLink for + # TRANSFER transactions return cls(asset.get('data'), asset['id'], asset.get('divisible', False), asset.get('updatable', False), @@ -706,8 +712,14 @@ class Transaction(object): # 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) + + if self.operation == self.CREATE: + amount = sum([condition.amount for condition in self.conditions]) + self.asset._validate_asset(amount=amount) + else: + # In transactions other then `CREATE` we don't know if its a divisible asset + # or not, so we cannot validate the amount here + self.asset._validate_asset() @classmethod @@ -756,6 +768,7 @@ class Transaction(object): # generate_conditions for owner_after in owners_after: + # TODO: Check types so this doesn't fail unpacking pub_keys, amount = owner_after conds.append(Condition.generate(pub_keys, amount)) @@ -988,13 +1001,18 @@ class Transaction(object): key_pairs = {gen_public_key(SigningKey(private_key)): SigningKey(private_key) for private_key in private_keys} - zippedIO = enumerate(zip(self.fulfillments, self.conditions)) - for index, (fulfillment, condition) in zippedIO: + # TODO: What does the conditions of this transaction have to do with the + # fulfillments, and why does this enforce for the number of fulfillments + # and conditions to be the same? + # TODO: Need to check how this was done before common but I from what I remember we + # included the condition that we were fulfilling in the message to be signed. + # zippedIO = enumerate(zip(self.fulfillments, self.conditions)) + for index, fulfillment in enumerate(self.fulfillments): # NOTE: We clone the current transaction but only add the condition # and fulfillment we're currently working on plus all # previously signed ones. tx_partial = Transaction(self.operation, self.asset, [fulfillment], - [condition], self.metadata, + self.conditions, self.metadata, self.timestamp, self.version) tx_partial_dict = tx_partial.to_dict() @@ -1157,8 +1175,9 @@ class Transaction(object): """Splits multiple IO Transactions into partial single IO Transactions. """ + # TODO: Understand how conditions are being handled tx = Transaction(self.operation, self.asset, [fulfillment], - [condition], self.metadata, self.timestamp, + self.conditions, self.metadata, self.timestamp, self.version) tx_dict = tx.to_dict() tx_dict = Transaction._remove_signatures(tx_dict) diff --git a/tests/assets/test_divisible_assets.py b/tests/assets/test_divisible_assets.py index bb5ed5ec..40f61cba 100644 --- a/tests/assets/test_divisible_assets.py +++ b/tests/assets/test_divisible_assets.py @@ -210,8 +210,8 @@ def test_single_in_single_own_single_out_multiple_own_transfer(b, user_vk, # TRANSFER tx_transfer = Transaction.transfer(tx_create.to_inputs(), - [([b.me, b.me], 100)], - asset=tx_create.asset) + [([b.me, b.me], 100)], + asset=tx_create.asset) tx_transfer_signed = tx_transfer.sign([user_sk]) assert tx_transfer_signed.validate(b) == tx_transfer_signed @@ -251,8 +251,8 @@ def test_single_in_single_own_multiple_out_mix_own_transfer(b, user_vk, # TRANSFER tx_transfer = Transaction.transfer(tx_create.to_inputs(), - [([b.me], 50), ([b.me, b.me], 50)], - asset=tx_create.asset) + [([b.me], 50), ([b.me, b.me], 50)], + asset=tx_create.asset) tx_transfer_signed = tx_transfer.sign([user_sk]) assert tx_transfer_signed.validate(b) == tx_transfer_signed @@ -267,5 +267,346 @@ def test_single_in_single_own_multiple_out_mix_own_transfer(b, user_vk, assert len(tx_transfer_signed.fulfillments) == 1 -#def test_single_in_multiple_own_single_out_single_own_create(b, user_vk): +# TRANSFER divisible asset +# Single input +# Multiple owners_before +# Single output +# Single owners_after +@pytest.mark.usefixtures('inputs') +def test_single_in_multiple_own_single_out_single_own_transfer(b, user_vk, + user_sk): + from bigchaindb.models import Transaction + from bigchaindb.common.transaction import Asset + + # CREATE divisible asset + asset = Asset(divisible=True) + tx_create = Transaction.create([b.me], [([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 + tx_transfer = Transaction.transfer(tx_create.to_inputs(), [([b.me], 100)], + asset=tx_create.asset) + tx_transfer_signed = tx_transfer.sign([b.me_private, user_sk]) + + assert tx_transfer_signed.validate(b) == tx_transfer_signed + assert len(tx_transfer_signed.conditions) == 1 + assert tx_transfer_signed.conditions[0].amount == 100 + assert len(tx_transfer_signed.fulfillments) == 1 + + ffill = tx_transfer_signed.fulfillments[0].fulfillment.to_dict() + assert 'subfulfillments' in ffill + assert len(ffill['subfulfillments']) == 2 + + +# TRANSFER divisible asset +# Multiple inputs +# Single owners_before per input +# Single output +# Single owners_after +@pytest.mark.usefixtures('inputs') +def test_multiple_in_single_own_single_out_single_own_transfer(b, user_vk, + user_sk): + from bigchaindb.models import Transaction + from bigchaindb.common.transaction import Asset + + # CREATE divisible asset + asset = Asset(divisible=True) + tx_create = Transaction.create([b.me], [([user_vk], 50), ([user_vk], 50)], + 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 + tx_transfer = Transaction.transfer(tx_create.to_inputs(), [([b.me], 100)], + asset=tx_create.asset) + tx_transfer_signed = tx_transfer.sign([user_sk]) + + assert tx_transfer_signed.validate(b) + assert len(tx_transfer_signed.conditions) == 1 + assert tx_transfer_signed.conditions[0].amount == 100 + assert len(tx_transfer_signed.fulfillments) == 2 + + +# TRANSFER divisible asset +# Multiple inputs +# Multiple owners_before per input +# Single output +# Single owners_after +@pytest.mark.usefixtures('inputs') +def test_multiple_in_multiple_own_single_out_single_own_transfer(b, user_vk, + user_sk): + from bigchaindb.models import Transaction + from bigchaindb.common.transaction import Asset + + # CREATE divisible asset + asset = Asset(divisible=True) + tx_create = Transaction.create([b.me], + [([user_vk, b.me], 50), + ([user_vk, b.me], 50)], + 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 + tx_transfer = Transaction.transfer(tx_create.to_inputs(), [([b.me], 100)], + asset=tx_create.asset) + tx_transfer_signed = tx_transfer.sign([b.me_private, user_sk]) + + assert tx_transfer_signed.validate(b) + assert len(tx_transfer_signed.conditions) == 1 + assert tx_transfer_signed.conditions[0].amount == 100 + assert len(tx_transfer_signed.fulfillments) == 2 + + ffill_fid0 = tx_transfer_signed.fulfillments[0].fulfillment.to_dict() + ffill_fid1 = tx_transfer_signed.fulfillments[1].fulfillment.to_dict() + assert 'subfulfillments' in ffill_fid0 + assert 'subfulfillments' in ffill_fid1 + assert len(ffill_fid0['subfulfillments']) == 2 + assert len(ffill_fid1['subfulfillments']) == 2 + + + +# TRANSFER divisible asset +# Multiple inputs +# Mix: one input with a single owners_before, one input with multiple +# owners_before +# Single output +# Single owners_after +@pytest.mark.usefixtures('inputs') +def test_muiltiple_in_mix_own_multiple_out_single_own_transfer(b, user_vk, + user_sk): + from bigchaindb.models import Transaction + from bigchaindb.common.transaction import Asset + + # CREATE divisible asset + asset = Asset(divisible=True) + tx_create = Transaction.create([b.me], + [([user_vk], 50), + ([user_vk, b.me], 50)], + 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 + tx_transfer = Transaction.transfer(tx_create.to_inputs(), [([b.me], 100)], + asset=tx_create.asset) + tx_transfer_signed = tx_transfer.sign([b.me_private, user_sk]) + + assert tx_transfer_signed.validate(b) == tx_transfer_signed + assert len(tx_transfer_signed.conditions) == 1 + assert tx_transfer_signed.conditions[0].amount == 100 + assert len(tx_transfer_signed.fulfillments) == 2 + + ffill_fid0 = tx_transfer_signed.fulfillments[0].fulfillment.to_dict() + ffill_fid1 = tx_transfer_signed.fulfillments[1].fulfillment.to_dict() + assert 'subfulfillments' not in ffill_fid0 + assert 'subfulfillments' in ffill_fid1 + assert len(ffill_fid1['subfulfillments']) == 2 + + +# TRANSFER divisible asset +# Multiple inputs +# Mix: one input with a single owners_before, one input with multiple +# owners_before +# Multiple outputs +# Mix: one output with a single owners_after, one output with multiple +# owners_after +@pytest.mark.usefixtures('inputs') +def test_muiltiple_in_mix_own_multiple_out_mix_own_transfer(b, user_vk, + user_sk): + from bigchaindb.models import Transaction + from bigchaindb.common.transaction import Asset + + # CREATE divisible asset + asset = Asset(divisible=True) + tx_create = Transaction.create([b.me], + [([user_vk], 50), + ([user_vk, b.me], 50)], + 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 + tx_transfer = Transaction.transfer(tx_create.to_inputs(), + [([b.me], 50), ([b.me, user_vk], 50)], + asset=tx_create.asset) + tx_transfer_signed = tx_transfer.sign([b.me_private, user_sk]) + + assert tx_transfer_signed.validate(b) == tx_transfer_signed + assert len(tx_transfer_signed.conditions) == 2 + assert tx_transfer_signed.conditions[0].amount == 50 + assert tx_transfer_signed.conditions[1].amount == 50 + assert len(tx_transfer_signed.fulfillments) == 2 + + cond_cid0 = tx_transfer_signed.conditions[0].to_dict() + cond_cid1 = tx_transfer_signed.conditions[1].to_dict() + assert 'subfulfillments' not in cond_cid0['condition']['details'] + assert 'subfulfillments' in cond_cid1['condition']['details'] + assert len(cond_cid1['condition']['details']['subfulfillments']) == 2 + + ffill_fid0 = tx_transfer_signed.fulfillments[0].fulfillment.to_dict() + ffill_fid1 = tx_transfer_signed.fulfillments[1].fulfillment.to_dict() + assert 'subfulfillments' not in ffill_fid0 + assert 'subfulfillments' in ffill_fid1 + assert len(ffill_fid1['subfulfillments']) == 2 + + +# TRANSFER divisible asset +# Multiple inputs from different transactions +# Single owners_before +# Single output +# Single owners_after +@pytest.mark.usefixtures('inputs') +def test_multiple_in_different_transactions(b, user_vk, user_sk): + from bigchaindb.models import Transaction + from bigchaindb.common.transaction import Asset + + # CREATE divisible asset + # `b` creates a divisible asset and assigns 50 shares to `b` and + # 50 shares to `user_vk` + asset = Asset(divisible=True) + tx_create = Transaction.create([b.me], + [([user_vk], 50), + ([b.me], 50)], + 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 divisible asset + # `b` transfers its 50 shares to `user_vk` + # after this transaction `user_vk` will have a total of 100 shares + # split across two different transactions + tx_transfer1 = Transaction.transfer([tx_create.to_inputs()[1]], + [([user_vk], 50)], + asset=tx_create.asset) + tx_transfer1_signed = tx_transfer1.sign([b.me_private]) + # create block + block = b.create_block([tx_transfer1_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 + # `user_vk` combines two different transaction with 50 shares each and + # transfers a total of 100 shares back to `b` + tx_transfer2 = Transaction.transfer([tx_create.to_inputs()[0], + tx_transfer1.to_inputs()[0]], + [([b.me], 100)], + asset=tx_create.asset) + tx_transfer2_signed = tx_transfer2.sign([user_sk]) + + assert tx_transfer2_signed.validate(b) == tx_transfer2_signed + assert len(tx_transfer2_signed.conditions) == 1 + assert tx_transfer2_signed.conditions[0].amount == 100 + assert len(tx_transfer2_signed.fulfillments) == 2 + + fid0_input = tx_transfer2_signed.fulfillments[0].to_dict()['input']['txid'] + fid1_input = tx_transfer2_signed.fulfillments[1].to_dict()['input']['txid'] + assert fid0_input == tx_create.id + assert fid1_input == tx_transfer1.id + + +@pytest.mark.usefixtures('inputs') +def test_transaction_unfulfilled_fulfillments(b, user_vk, + user_sk): + from bigchaindb.models import Transaction + from bigchaindb.common.transaction import Asset + + # CREATE divisible asset + asset = Asset(divisible=True) + tx_create = Transaction.create([b.me], + [([user_vk, b.me], 50), + ([user_vk, b.me], 50)], + 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 + tx_transfer = Transaction.transfer(tx_create.to_inputs(), [([b.me], 100)], + asset=tx_create.asset) + tx_transfer_signed = tx_transfer.sign([b.me_private, user_sk]) + + # TODO: This transaction has unfulfilled fulfillments and should be + # 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')) +@pytest.mark.usefixtures('inputs') +def test_threshold_same_public_key(b, user_vk, user_sk): + # If we try to fulfill a threshold condition where each subcondition has + # the same key get_subcondition_from_vk will always return the first + # subcondition. This means that only the 1st subfulfillment will be + # generated + + from bigchaindb.models import Transaction + from bigchaindb.common.transaction import Asset + + # CREATE divisible asset + asset = Asset(divisible=True) + tx_create = Transaction.create([b.me], [([user_vk, 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 + tx_transfer = Transaction.transfer(tx_create.to_inputs(), [([b.me], 100)], + asset=tx_create.asset) + tx_transfer_signed = tx_transfer.sign([user_sk, user_sk]) + + assert tx_transfer_signed.validate(b) == tx_transfer_signed From a212aba35b3409f8d874a3e70c6596fe6bbd3538 Mon Sep 17 00:00:00 2001 From: Rodolphe Marques Date: Sun, 6 Nov 2016 20:00:47 +0100 Subject: [PATCH 08/54] 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')) From 3ac530617c1f3c2f0f67ab8de3d134e3987b774d Mon Sep 17 00:00:00 2001 From: Rodolphe Marques Date: Sun, 6 Nov 2016 22:35:39 +0100 Subject: [PATCH 09/54] Fixed some tests --- bigchaindb/common/transaction.py | 8 +++- tests/assets/test_digital_assets.py | 70 ++++++++++----------------- tests/common/test_transaction.py | 74 ++++++++++++++++++----------- tests/test_models.py | 14 +++--- 4 files changed, 83 insertions(+), 83 deletions(-) diff --git a/bigchaindb/common/transaction.py b/bigchaindb/common/transaction.py index 7f18d1c4..eb2abcd4 100644 --- a/bigchaindb/common/transaction.py +++ b/bigchaindb/common/transaction.py @@ -1194,8 +1194,12 @@ class Transaction(object): # raise ValueError('Fulfillments, conditions and ' # 'input_condition_uris must have the same count') # else: - partial_transactions = map(gen_tx, self.fulfillments, - self.conditions, input_condition_uris) + if not fulfillments_count == input_condition_uris_count: + raise ValueError('Fulfillments and ' + 'input_condition_uris must have the same count') + else: + partial_transactions = map(gen_tx, self.fulfillments, + self.conditions, input_condition_uris) return all(partial_transactions) @staticmethod diff --git a/tests/assets/test_digital_assets.py b/tests/assets/test_digital_assets.py index 46a4463a..85d3a60a 100644 --- a/tests/assets/test_digital_assets.py +++ b/tests/assets/test_digital_assets.py @@ -11,7 +11,7 @@ def test_asset_transfer(b, user_vk, user_sk): tx_input = b.get_owned_ids(user_vk).pop() tx_create = b.get_transaction(tx_input.txid) - 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]) @@ -20,61 +20,40 @@ def test_asset_transfer(b, user_vk, user_sk): def test_validate_bad_asset_creation(b, user_vk): - from bigchaindb.models import Transaction + from bigchaindb.models import Transaction, Asset # `divisible` needs to be a boolean - tx = Transaction.create([b.me], [user_vk]) + tx = Transaction.create([b.me], [([user_vk], 1)]) tx.asset.divisible = 1 - tx_signed = tx.sign([b.me_private]) + with patch.object(Asset, '_validate_asset', return_value=None): + tx_signed = tx.sign([b.me_private]) with pytest.raises(TypeError): tx_signed.validate(b) # `refillable` needs to be a boolean - tx = Transaction.create([b.me], [user_vk]) + tx = Transaction.create([b.me], [([user_vk], 1)]) tx.asset.refillable = 1 - tx_signed = tx.sign([b.me_private]) + with patch.object(Asset, '_validate_asset', return_value=None): + tx_signed = tx.sign([b.me_private]) with pytest.raises(TypeError): b.validate_transaction(tx_signed) # `updatable` needs to be a boolean - tx = Transaction.create([b.me], [user_vk]) + tx = Transaction.create([b.me], [([user_vk], 1)]) tx.asset.updatable = 1 - tx_signed = tx.sign([b.me_private]) + with patch.object(Asset, '_validate_asset', return_value=None): + tx_signed = tx.sign([b.me_private]) with pytest.raises(TypeError): b.validate_transaction(tx_signed) # `data` needs to be a dictionary - tx = Transaction.create([b.me], [user_vk]) + tx = Transaction.create([b.me], [([user_vk], 1)]) tx.asset.data = 'a' - tx_signed = tx.sign([b.me_private]) + with patch.object(Asset, '_validate_asset', return_value=None): + 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']) - tx_signed = b.sign_transaction(tx, b.me_private) - with pytest.raises(TypeError): - b.validate_transaction(tx_signed) - - tx = b.create_transaction(b.me, user_vk, None, 'CREATE') - tx['transaction']['conditions'][0]['amount'] = 2 - tx['transaction']['asset'].update({'divisible': False}) - tx['id'] = get_hash_data(tx['transaction']) - tx_signed = b.sign_transaction(tx, b.me_private) - with pytest.raises(AmountError): - b.validate_transaction(tx_signed) - - tx = b.create_transaction(b.me, user_vk, None, 'CREATE') - tx['transaction']['conditions'][0]['amount'] = 0 - tx['id'] = get_hash_data(tx['transaction']) - 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): @@ -83,7 +62,7 @@ def test_validate_transfer_asset_id_mismatch(b, user_vk, 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_transfer = Transaction.transfer(tx_create.to_inputs(), [([user_vk], 1)], tx_create.asset) tx_transfer.asset.data_id = 'aaa' tx_transfer_signed = tx_transfer.sign([user_sk]) @@ -94,7 +73,7 @@ def test_validate_transfer_asset_id_mismatch(b, user_vk, user_sk): def test_get_asset_id_create_transaction(b, user_vk): from bigchaindb.models import Transaction, Asset - tx_create = Transaction.create([b.me], [user_vk]) + tx_create = Transaction.create([b.me], [([user_vk], 1)]) asset_id = Asset.get_asset_id(tx_create) assert asset_id == tx_create.asset.data_id @@ -107,7 +86,7 @@ def test_get_asset_id_transfer_transaction(b, user_vk, user_sk): tx_create = b.get_owned_ids(user_vk).pop() tx_create = b.get_transaction(tx_create.txid) # 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 a block @@ -125,8 +104,8 @@ def test_asset_id_mismatch(b, user_vk): from bigchaindb.models import Transaction, Asset from bigchaindb.common.exceptions import AssetIdMismatch - tx1 = Transaction.create([b.me], [user_vk]) - tx2 = Transaction.create([b.me], [user_vk]) + tx1 = Transaction.create([b.me], [([user_vk], 1)]) + tx2 = Transaction.create([b.me], [([user_vk], 1)]) with pytest.raises(AssetIdMismatch): Asset.get_asset_id([tx1, tx2]) @@ -190,6 +169,7 @@ def test_get_asset_by_id(b, user_vk, user_sk): 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 @@ -198,19 +178,19 @@ def test_create_invalid_divisible_asset(b, user_vk, user_sk): # Transaction.__init__ should raise an exception asset = Asset(divisible=False) with pytest.raises(AmountError): - Transaction.create([user_vk], [user_vk], asset=asset, amount=2) + Transaction.create([user_vk], [([user_vk], 2)], asset=asset) # 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) + Transaction.create([user_vk], [([user_vk], 1)], asset=asset) # 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 = Transaction.create([user_vk], [([user_vk], 2)], asset=asset) tx_signed = tx.sign([user_sk]) with pytest.raises(AmountError): tx_signed.validate(b) @@ -218,7 +198,7 @@ def test_create_invalid_divisible_asset(b, user_vk, user_sk): 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 = Transaction.create([user_vk], [([user_vk], 1)], asset=asset) tx_signed = tx.sign([user_sk]) with pytest.raises(AmountError): tx_signed.validate(b) @@ -229,6 +209,6 @@ 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 = Transaction.create([user_vk], [([user_vk], 2)], asset=asset) tx_signed = tx.sign([user_sk]) assert b.is_valid_transaction(tx_signed) diff --git a/tests/common/test_transaction.py b/tests/common/test_transaction.py index 04bd7eb5..b14d2588 100644 --- a/tests/common/test_transaction.py +++ b/tests/common/test_transaction.py @@ -1,4 +1,5 @@ from pytest import raises, mark +from unittest.mock import patch def test_fulfillment_serialization(ffill_uri, user_pub): @@ -166,7 +167,7 @@ def test_generate_conditions_split_half_recursive(user_pub, user2_pub, expected_threshold.add_subfulfillment(expected_simple3) expected.add_subfulfillment(expected_threshold) - cond = Condition.generate([user_pub, [user2_pub, expected_simple3]]) + cond = Condition.generate([user_pub, [user2_pub, expected_simple3]], 1) assert cond.fulfillment.to_dict() == expected.to_dict() @@ -188,7 +189,7 @@ def test_generate_conditions_split_half_recursive_custom_threshold(user_pub, expected.add_subfulfillment(expected_threshold) cond = Condition.generate(([user_pub, ([user2_pub, expected_simple3], 1)], - 1)) + 1), 1) assert cond.fulfillment.to_dict() == expected.to_dict() @@ -208,7 +209,7 @@ def test_generate_conditions_split_half_single_owner(user_pub, user2_pub, expected.add_subfulfillment(expected_threshold) expected.add_subfulfillment(expected_simple1) - cond = Condition.generate([[expected_simple2, user3_pub], user_pub]) + cond = Condition.generate([[expected_simple2, user3_pub], user_pub], 1) assert cond.fulfillment.to_dict() == expected.to_dict() @@ -225,7 +226,7 @@ def test_generate_conditions_flat_ownage(user_pub, user2_pub, user3_pub): expected.add_subfulfillment(expected_simple2) expected.add_subfulfillment(expected_simple3) - cond = Condition.generate([user_pub, user2_pub, expected_simple3]) + cond = Condition.generate([user_pub, user2_pub, expected_simple3], 1) assert cond.fulfillment.to_dict() == expected.to_dict() @@ -234,7 +235,7 @@ def test_generate_conditions_single_owner(user_pub): from cryptoconditions import Ed25519Fulfillment expected = Ed25519Fulfillment(public_key=user_pub) - cond = Condition.generate([user_pub]) + cond = Condition.generate([user_pub], 1) assert cond.fulfillment.to_dict() == expected.to_dict() @@ -244,7 +245,7 @@ def test_generate_conditions_single_owner_with_condition(user_pub): from cryptoconditions import Ed25519Fulfillment expected = Ed25519Fulfillment(public_key=user_pub) - cond = Condition.generate([expected]) + cond = Condition.generate([expected], 1) assert cond.fulfillment.to_dict() == expected.to_dict() @@ -270,7 +271,7 @@ def test_generate_threshold_condition_with_hashlock(user_pub, user2_pub, expected_sub.add_subfulfillment(hashlock) expected.add_subfulfillment(expected_simple3) - cond = Condition.generate([[user_pub, hashlock], expected_simple3]) + cond = Condition.generate([[user_pub, hashlock], expected_simple3], 1) assert cond.fulfillment.to_dict() == expected.to_dict() @@ -279,13 +280,13 @@ def test_generate_conditions_invalid_parameters(user_pub, user2_pub, from bigchaindb.common.transaction import Condition with raises(ValueError): - Condition.generate([]) + Condition.generate([], 1) with raises(TypeError): - Condition.generate('not a list') + Condition.generate('not a list', 1) with raises(ValueError): - Condition.generate([[user_pub, [user2_pub, [user3_pub]]]]) + Condition.generate([[user_pub, [user2_pub, [user3_pub]]]], 1) with raises(ValueError): - Condition.generate([[user_pub]]) + Condition.generate([[user_pub]], 1) def test_invalid_transaction_initialization(): @@ -321,7 +322,8 @@ def test_invalid_transaction_initialization(): def test_create_default_asset_on_tx_initialization(): from bigchaindb.common.transaction import Transaction, Asset - tx = Transaction(Transaction.CREATE, None) + with patch.object(Asset, '_validate_asset', return_value=None): + tx = Transaction(Transaction.CREATE, None) expected = Asset() asset = tx.asset @@ -513,7 +515,8 @@ def test_cast_transaction_link_to_boolean(): def test_add_fulfillment_to_tx(user_ffill): from bigchaindb.common.transaction import Transaction, Asset - tx = Transaction(Transaction.CREATE, Asset(), [], []) + with patch.object(Asset, '_validate_asset', return_value=None): + tx = Transaction(Transaction.CREATE, Asset(), [], []) tx.add_fulfillment(user_ffill) assert len(tx.fulfillments) == 1 @@ -522,7 +525,8 @@ def test_add_fulfillment_to_tx(user_ffill): def test_add_fulfillment_to_tx_with_invalid_parameters(): from bigchaindb.common.transaction import Transaction, Asset - tx = Transaction(Transaction.CREATE, Asset()) + with patch.object(Asset, '_validate_asset', return_value=None): + tx = Transaction(Transaction.CREATE, Asset()) with raises(TypeError): tx.add_fulfillment('somewronginput') @@ -530,7 +534,8 @@ def test_add_fulfillment_to_tx_with_invalid_parameters(): def test_add_condition_to_tx(user_cond): from bigchaindb.common.transaction import Transaction, Asset - tx = Transaction(Transaction.CREATE, Asset()) + with patch.object(Asset, '_validate_asset', return_value=None): + tx = Transaction(Transaction.CREATE, Asset()) tx.add_condition(user_cond) assert len(tx.conditions) == 1 @@ -539,7 +544,8 @@ def test_add_condition_to_tx(user_cond): def test_add_condition_to_tx_with_invalid_parameters(): from bigchaindb.common.transaction import Transaction, Asset - tx = Transaction(Transaction.CREATE, Asset(), [], []) + with patch.object(Asset, '_validate_asset', return_value=None): + tx = Transaction(Transaction.CREATE, Asset(), [], []) with raises(TypeError): tx.add_condition('somewronginput') @@ -608,12 +614,14 @@ def test_validate_fulfillment_with_invalid_parameters(utx): input_conditions) is False +@mark.skip(reason='Talk to @TimDaub') def test_validate_multiple_fulfillments(user_ffill, user_cond, user_priv): from copy import deepcopy from bigchaindb.common.crypto import SigningKey from bigchaindb.common.transaction import Transaction, Asset + # TODO: Why is there a fulfillment in the conditions list tx = Transaction(Transaction.CREATE, Asset(), [user_ffill, deepcopy(user_ffill)], [user_ffill, deepcopy(user_cond)]) @@ -674,7 +682,7 @@ def test_multiple_fulfillment_validation_of_transfer_tx(user_ffill, user_cond, Fulfillment, Condition, Asset) from cryptoconditions import Ed25519Fulfillment - tx = Transaction(Transaction.CREATE, Asset(), + tx = Transaction(Transaction.CREATE, Asset(divisible=True), [user_ffill, deepcopy(user_ffill)], [user_cond, deepcopy(user_cond)]) tx.sign([user_priv]) @@ -692,6 +700,7 @@ def test_multiple_fulfillment_validation_of_transfer_tx(user_ffill, user_cond, assert transfer_tx.fulfillments_valid(tx.conditions) is True +@mark.skip(reason='Ask @TimDaub') def test_validate_fulfillments_of_transfer_tx_with_invalid_params(transfer_tx, cond_uri, utx, @@ -715,6 +724,7 @@ def test_validate_fulfillments_of_transfer_tx_with_invalid_params(transfer_tx, with raises(TypeError): transfer_tx.operation = "Operation that doesn't exist" transfer_tx.fulfillments_valid([utx.conditions[0]]) + # TODO: Why should this raise a ValueError? with raises(ValueError): tx = utx.sign([user_priv]) tx.conditions = [] @@ -754,7 +764,8 @@ def test_create_create_transaction_single_io(user_cond, user_pub, data, } asset = Asset(data, data_id) - tx = Transaction.create([user_pub], [user_pub], data, asset).to_dict() + tx = Transaction.create([user_pub], [([user_pub], 1)], + data, asset).to_dict() tx.pop('id') tx['transaction']['metadata'].pop('id') tx['transaction'].pop('timestamp') @@ -766,7 +777,7 @@ def test_create_create_transaction_single_io(user_cond, user_pub, data, def test_validate_single_io_create_transaction(user_pub, user_priv, data): from bigchaindb.common.transaction import Transaction, Asset - tx = Transaction.create([user_pub], [user_pub], data, Asset()) + tx = Transaction.create([user_pub], [([user_pub], 1)], data, Asset()) tx = tx.sign([user_priv]) assert tx.fulfillments_valid() is True @@ -816,14 +827,15 @@ def test_create_create_transaction_multiple_io(user_cond, user2_cond, user_pub, assert tx == expected -# @mark.skip(reason='Multiple inputs and outputs in CREATE not supported') -# TODO: Add digital assets def test_validate_multiple_io_create_transaction(user_pub, user_priv, user2_pub, user2_priv): - from bigchaindb.common.transaction import Transaction + from bigchaindb.common.transaction import Transaction, Asset - tx = Transaction.create([user_pub, user2_pub], [user_pub, user2_pub], - metadata={'message': 'hello'}) + # TODO: Fix multiple owners_before in create transactions + tx = Transaction.create([user_pub, user2_pub], + [([user_pub], 1), ([user2_pub], 1)], + metadata={'message': 'hello'}, + asset=Asset(divisible=True)) tx = tx.sign([user_priv, user2_priv]) assert tx.fulfillments_valid() is True @@ -995,7 +1007,7 @@ def test_create_transfer_transaction_single_io(tx, user_pub, user2_pub, } inputs = tx.to_inputs([0]) asset = Asset(None, data_id) - transfer_tx = Transaction.transfer(inputs, [user2_pub], asset=asset) + transfer_tx = Transaction.transfer(inputs, [([user2_pub], 1)], asset=asset) transfer_tx = transfer_tx.sign([user_priv]) transfer_tx = transfer_tx.to_dict() transfer_tx_body = transfer_tx['transaction'] @@ -1093,14 +1105,18 @@ def test_create_transfer_with_invalid_parameters(): def test_cant_add_empty_condition(): - from bigchaindb.common.transaction import Transaction - tx = Transaction(Transaction.CREATE, None) + from bigchaindb.common.transaction import Transaction, Asset + + with patch.object(Asset, '_validate_asset', return_value=None): + tx = Transaction(Transaction.CREATE, None) with raises(TypeError): tx.add_condition(None) def test_cant_add_empty_fulfillment(): - from bigchaindb.common.transaction import Transaction - tx = Transaction(Transaction.CREATE, None) + from bigchaindb.common.transaction import Transaction, Asset + + with patch.object(Asset, '_validate_asset', return_value=None): + tx = Transaction(Transaction.CREATE, None) with raises(TypeError): tx.add_fulfillment(None) diff --git a/tests/test_models.py b/tests/test_models.py index 5033aebb..534052f9 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -5,7 +5,7 @@ class TestTransactionModel(object): def test_validating_an_invalid_transaction(self, b): from bigchaindb.models import Transaction - tx = Transaction.create([b.me], [b.me]) + tx = Transaction.create([b.me], [([b.me], 1)]) tx.operation = 'something invalid' with raises(TypeError): @@ -41,7 +41,7 @@ class TestBlockModel(object): from bigchaindb.common.util import gen_timestamp, serialize from bigchaindb.models import Block, Transaction - transactions = [Transaction.create([b.me], [b.me])] + transactions = [Transaction.create([b.me], [([b.me], 1)])] timestamp = gen_timestamp() voters = ['Qaaa', 'Qbbb'] expected_block = { @@ -73,7 +73,7 @@ class TestBlockModel(object): from bigchaindb.common.util import gen_timestamp, serialize from bigchaindb.models import Block, Transaction - transactions = [Transaction.create([b.me], [b.me])] + transactions = [Transaction.create([b.me], [([b.me], 1)])] timestamp = gen_timestamp() voters = ['Qaaa', 'Qbbb'] expected = Block(transactions, b.me, timestamp, voters) @@ -113,7 +113,7 @@ class TestBlockModel(object): from bigchaindb.common.util import gen_timestamp, serialize from bigchaindb.models import Block, Transaction - transactions = [Transaction.create([b.me], [b.me])] + transactions = [Transaction.create([b.me], [([b.me], 1)])] timestamp = gen_timestamp() voters = ['Qaaa', 'Qbbb'] @@ -136,7 +136,7 @@ class TestBlockModel(object): def test_compare_blocks(self, b): from bigchaindb.models import Block, Transaction - transactions = [Transaction.create([b.me], [b.me])] + transactions = [Transaction.create([b.me], [([b.me], 1)])] assert Block() != 'invalid comparison' assert Block(transactions) == Block(transactions) @@ -146,7 +146,7 @@ class TestBlockModel(object): from bigchaindb.common.util import gen_timestamp, serialize from bigchaindb.models import Block, Transaction - transactions = [Transaction.create([b.me], [b.me])] + transactions = [Transaction.create([b.me], [([b.me], 1)])] timestamp = gen_timestamp() voters = ['Qaaa', 'Qbbb'] expected_block = { @@ -168,7 +168,7 @@ class TestBlockModel(object): from unittest.mock import Mock from bigchaindb.models import Transaction - tx = Transaction.create([b.me], [b.me]) + tx = Transaction.create([b.me], [([b.me], 1)]) block = b.create_block([tx]) has_previous_vote = Mock() From 6e2ac1df035e3c9b16ebc0b0041420f82aeb3a0b Mon Sep 17 00:00:00 2001 From: Rodolphe Marques Date: Mon, 7 Nov 2016 10:04:09 +0100 Subject: [PATCH 10/54] remove unused code --- bigchaindb/common/transaction.py | 58 ++++++-------------------------- 1 file changed, 11 insertions(+), 47 deletions(-) diff --git a/bigchaindb/common/transaction.py b/bigchaindb/common/transaction.py index eb2abcd4..24f8fb06 100644 --- a/bigchaindb/common/transaction.py +++ b/bigchaindb/common/transaction.py @@ -761,6 +761,17 @@ class Transaction(object): if not isinstance(owners_after, list): raise TypeError('`owners_after` must be a list instance') + if (len(owners_before) > 0 and len(owners_after) == 0 and + time_expire is not None): + raise NotImplementedError('Timeout conditions will be implemented ' + 'later') + elif (len(owners_before) > 0 and len(owners_after) == 0 and + secret is None): + raise ValueError('Define a secret to create a hashlock condition') + + else: + raise ValueError("These are not the cases you're looking for ;)") + metadata = Metadata(metadata) ffils = [] @@ -777,53 +788,6 @@ class Transaction(object): return cls(cls.CREATE, asset, ffils, conds, metadata) - - - if len(owners_before) == len(owners_after) and len(owners_after) == 1: - # NOTE: Standard case, one owner before, one after. - # NOTE: For this case its sufficient to use the same - # 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, amount=amount) - return cls(cls.CREATE, asset, [ffill_tx], [cond_tx], metadata) - - elif len(owners_before) == len(owners_after) and len(owners_after) > 1: - ffills = [Fulfillment(Ed25519Fulfillment(public_key=owner_before), - [owner_before]) - for owner_before in owners_before] - conds = [Condition.generate([owners], amount) - for owners in owners_after] - return cls(cls.CREATE, asset, ffills, conds, metadata) - - elif len(owners_before) == 1 and len(owners_after) > 1: - # NOTE: Multiple owners case - cond_tx = Condition.generate(owners_after, 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) - - elif (len(owners_before) == 1 and len(owners_after) == 0 and - secret is not None): - # NOTE: Hashlock condition case - hashlock = PreimageSha256Fulfillment(preimage=secret) - 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) - - elif (len(owners_before) > 0 and len(owners_after) == 0 and - time_expire is not None): - raise NotImplementedError('Timeout conditions will be implemented ' - 'later') - - elif (len(owners_before) > 0 and len(owners_after) == 0 and - secret is None): - raise ValueError('Define a secret to create a hashlock condition') - - else: - raise ValueError("These are not the cases you're looking for ;)") - @classmethod def transfer(cls, inputs, owners_after, asset, metadata=None): """A simple way to generate a `TRANSFER` transaction. From 9a5bc816d806592dc027dd5523bcf726eb224c83 Mon Sep 17 00:00:00 2001 From: Rodolphe Marques Date: Mon, 7 Nov 2016 15:26:42 +0100 Subject: [PATCH 11/54] re-added code to handle hashlock conditions --- bigchaindb/common/transaction.py | 41 +++++++++++++++++++++----------- 1 file changed, 27 insertions(+), 14 deletions(-) diff --git a/bigchaindb/common/transaction.py b/bigchaindb/common/transaction.py index 24f8fb06..6fb0d9a4 100644 --- a/bigchaindb/common/transaction.py +++ b/bigchaindb/common/transaction.py @@ -469,12 +469,12 @@ class Asset(object): Returns: :class:`~bigchaindb.common.transaction.Asset` """ - # TODO: This is not correct. If using Transaction.from_dict() from a - # TRANSFER transaction we only have information about the `id`, meaning - # that even if its a divisible asset, since the key does not exist if will be - # set to False by default. - # Maybe use something like an AssetLink similar to TransactionLink for - # TRANSFER transactions + # TODO: This is not correct. If using Transaction.from_dict() from a + # TRANSFER transaction we only have information about the `id`, + # meaning that even if its a divisible asset, since the key does + # not exist if will be set to False by default. + # Maybe use something like an AssetLink similar to + # TransactionLink for TRANSFER transactions return cls(asset.get('data'), asset['id'], asset.get('divisible', False), asset.get('updatable', False), @@ -488,11 +488,12 @@ class Asset(object): 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. + 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 + transactions (list): list of transaction usually inputs that should + have a matching asset_id Returns: str: uuid of the asset. @@ -509,7 +510,8 @@ class Asset(object): # 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.") + raise AssetIdMismatch(('All inputs of a transaction need' + ' to have the same asset id.')) return asset_ids.pop() def _validate_asset(self, amount=None): @@ -717,11 +719,10 @@ class Transaction(object): amount = sum([condition.amount for condition in self.conditions]) self.asset._validate_asset(amount=amount) else: - # In transactions other then `CREATE` we don't know if its a divisible asset - # or not, so we cannot validate the amount here + # In transactions other then `CREATE` we don't know if its a + # divisible asset or not, so we cannot validate the amount here self.asset._validate_asset() - @classmethod def create(cls, owners_before, owners_after, metadata=None, asset=None, secret=None, time_expire=None): @@ -762,7 +763,7 @@ class Transaction(object): raise TypeError('`owners_after` must be a list instance') if (len(owners_before) > 0 and len(owners_after) == 0 and - time_expire is not None): + time_expire is not None): raise NotImplementedError('Timeout conditions will be implemented ' 'later') elif (len(owners_before) > 0 and len(owners_after) == 0 and @@ -774,6 +775,18 @@ class Transaction(object): metadata = Metadata(metadata) + # TODO: Not sure there is a need to ensure that `owners_before == 1` + # TODO: For divisible assets we will need to create one hashlock + # condition per output + # if (len(owners_before) == 1 and len(owners_after) == 0 and + # secret is not None): + # # NOTE: Hashlock condition case + # hashlock = PreimageSha256Fulfillment(preimage=secret) + # 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) + ffils = [] conds = [] From 19cfe172eab73d018c6a0b4d6ec18a635ec88383 Mon Sep 17 00:00:00 2001 From: Rodolphe Marques Date: Tue, 8 Nov 2016 17:41:53 +0100 Subject: [PATCH 12/54] Added support for multiple owners_before in CREATE transactions Added some type checking Remove code for hashlocks and timelocks. They were partially implemented features that we need to revisit. --- bigchaindb/common/transaction.py | 59 ++++++++------------------- tests/assets/test_divisible_assets.py | 17 +++++--- tests/common/test_transaction.py | 8 +++- 3 files changed, 34 insertions(+), 50 deletions(-) diff --git a/bigchaindb/common/transaction.py b/bigchaindb/common/transaction.py index 6fb0d9a4..e1aa8245 100644 --- a/bigchaindb/common/transaction.py +++ b/bigchaindb/common/transaction.py @@ -102,10 +102,10 @@ class Fulfillment(object): @classmethod def generate(cls, owners_before): # TODO: write docstring - - if len(owners_before) == 1: - ffill = Ed25519Fulfillment(public_key=owners_before[0]) - return cls(ffill, owners_before) + # The amount here does not really matter. It is only use on the + # condition data model but here we only care about the fulfillment + condition = Condition.generate(owners_before, 1) + return cls(condition.fulfillment, condition.owners_after) @classmethod def from_dict(cls, ffill): @@ -308,14 +308,7 @@ class Condition(object): TypeError: If `owners_after` is not an instance of `list`. TypeError: If `owners_after` is an empty list. """ - # TODO: We probably want to remove the tuple logic for weights here - # again: - # github.com/bigchaindb/bigchaindb/issues/730#issuecomment-255144756 - if isinstance(owners_after, tuple): - owners_after, threshold = owners_after - else: - threshold = len(owners_after) - + threshold = len(owners_after) if not isinstance(amount, int): raise TypeError('`amount` must be a int') if not isinstance(owners_after, list): @@ -724,8 +717,7 @@ class Transaction(object): self.asset._validate_asset() @classmethod - def create(cls, owners_before, owners_after, metadata=None, asset=None, - secret=None, time_expire=None): + def create(cls, owners_before, owners_after, metadata=None, asset=None): # TODO: Update docstring """A simple way to generate a `CREATE` transaction. @@ -761,45 +753,28 @@ 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 (len(owners_before) > 0 and len(owners_after) == 0 and - time_expire is not None): - raise NotImplementedError('Timeout conditions will be implemented ' - 'later') - elif (len(owners_before) > 0 and len(owners_after) == 0 and - secret is None): - raise ValueError('Define a secret to create a hashlock condition') - - else: - raise ValueError("These are not the cases you're looking for ;)") + if len(owners_before) == 0: + raise ValueError('`owners_before` list cannot be empty') + if len(owners_after) == 0: + raise ValueError('`owners_after` list cannot be empty') metadata = Metadata(metadata) - - # TODO: Not sure there is a need to ensure that `owners_before == 1` - # TODO: For divisible assets we will need to create one hashlock - # condition per output - # if (len(owners_before) == 1 and len(owners_after) == 0 and - # secret is not None): - # # NOTE: Hashlock condition case - # hashlock = PreimageSha256Fulfillment(preimage=secret) - # 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) - - ffils = [] + ffills = [] conds = [] # generate_conditions for owner_after in owners_after: # TODO: Check types so this doesn't fail unpacking + if not isinstance(owner_after, tuple) or len(owner_after) != 2: + raise ValueError(('Each `owner_after` in the list is a tuple' + ' of `([], )`')) pub_keys, amount = owner_after conds.append(Condition.generate(pub_keys, amount)) # generate fulfillments - ffils.append(Fulfillment.generate(owners_before)) + ffills.append(Fulfillment.generate(owners_before)) - return cls(cls.CREATE, asset, ffils, conds, metadata) + return cls(cls.CREATE, asset, ffills, conds, metadata) @classmethod def transfer(cls, inputs, owners_after, asset, metadata=None): @@ -978,7 +953,7 @@ class Transaction(object): key_pairs = {gen_public_key(SigningKey(private_key)): SigningKey(private_key) for private_key in private_keys} - # TODO: What does the conditions of this transaction have to do with the + # TODO: What does the conditions of this transaction have to do with the # fulfillments, and why does this enforce for the number of fulfillments # and conditions to be the same? # TODO: Need to check how this was done before common but I from what I remember we diff --git a/tests/assets/test_divisible_assets.py b/tests/assets/test_divisible_assets.py index b227569a..aeed19e6 100644 --- a/tests/assets/test_divisible_assets.py +++ b/tests/assets/test_divisible_assets.py @@ -98,17 +98,22 @@ def test_single_in_single_own_multiple_out_mix_own_create(b, user_vk): # Single input # Multiple owners_before # Ouput combinations already tested above -# TODO: Support multiple owners_before in CREATE transactions -@pytest.mark.skip(reason=('CREATE transaction do not support multiple' - ' owners_before')) -def test_single_in_multiple_own_single_out_single_own_create(b, user_vk): +def test_single_in_multiple_own_single_out_single_own_create(b, user_vk, + user_sk): from bigchaindb.models import Transaction from bigchaindb.common.transaction import Asset asset = Asset(divisible=True) - tx = Transaction.create([b.me, b.me], [([user_vk], 100)], asset=asset) - tx_signed = tx.sign([b.me, b.me]) + tx = Transaction.create([b.me, user_vk], [([user_vk], 100)], asset=asset) + tx_signed = tx.sign([b.me_private, user_sk]) assert tx_signed.validate(b) == tx_signed + assert len(tx_signed.conditions) == 1 + assert tx_signed.conditions[0].amount == 100 + assert len(tx_signed.fulfillments) == 1 + + ffill = tx_signed.fulfillments[0].fulfillment.to_dict() + assert 'subfulfillments' in ffill + assert len(ffill['subfulfillments']) == 2 # TRANSFER divisible asset diff --git a/tests/common/test_transaction.py b/tests/common/test_transaction.py index b14d2588..d5754b47 100644 --- a/tests/common/test_transaction.py +++ b/tests/common/test_transaction.py @@ -874,7 +874,8 @@ def test_create_create_transaction_threshold(user_pub, user2_pub, user3_pub, 'version': 1 } asset = Asset(data, data_id) - tx = Transaction.create([user_pub], [user_pub, user2_pub], data, asset) + tx = Transaction.create([user_pub], [([user_pub, user2_pub], 1)], + data, asset) tx_dict = tx.to_dict() tx_dict.pop('id') tx_dict['transaction']['metadata'].pop('id') @@ -888,11 +889,13 @@ def test_validate_threshold_create_transaction(user_pub, user_priv, user2_pub, data): from bigchaindb.common.transaction import Transaction, Asset - tx = Transaction.create([user_pub], [user_pub, user2_pub], data, Asset()) + tx = Transaction.create([user_pub], [([user_pub, user2_pub], 1)], + data, Asset()) tx = tx.sign([user_priv]) assert tx.fulfillments_valid() is True +@mark.skip(reason='Hashlocks are not implemented') def test_create_create_transaction_hashlock(user_pub, data, data_id): from cryptoconditions import PreimageSha256Fulfillment from bigchaindb.common.transaction import Transaction, Condition, Asset @@ -939,6 +942,7 @@ def test_create_create_transaction_hashlock(user_pub, data, data_id): assert tx == expected +@mark.skip(reson='Hashlocks are not implemented') def test_validate_hashlock_create_transaction(user_pub, user_priv, data): from bigchaindb.common.transaction import Transaction, Asset From e35d2899b55c7a21bf10ef3be9c37c1a5de03e42 Mon Sep 17 00:00:00 2001 From: Rodolphe Marques Date: Wed, 9 Nov 2016 13:32:18 +0100 Subject: [PATCH 13/54] Removed support for custom threshold Removed support for hashlocks Unskipped tests that were skipped waiting for divisible assets Fixed remaining tests --- bigchaindb/common/transaction.py | 31 +-- bigchaindb/pipelines/vote.py | 2 +- tests/assets/test_divisible_assets.py | 110 +++++---- tests/common/test_transaction.py | 125 ++++------ tests/conftest.py | 4 +- tests/db/test_bigchain_api.py | 310 +++++++------------------ tests/pipelines/test_block_creation.py | 14 +- tests/pipelines/test_election.py | 14 +- tests/pipelines/test_stale_monitor.py | 10 +- tests/pipelines/test_vote.py | 17 +- tests/web/test_basic_views.py | 12 +- 11 files changed, 251 insertions(+), 398 deletions(-) diff --git a/bigchaindb/common/transaction.py b/bigchaindb/common/transaction.py index e1aa8245..fc364bf7 100644 --- a/bigchaindb/common/transaction.py +++ b/bigchaindb/common/transaction.py @@ -346,14 +346,11 @@ class Condition(object): Returns: :class:`cryptoconditions.ThresholdSha256Fulfillment`: """ - if isinstance(current, tuple): - owners_after, threshold = current - else: - owners_after = current - try: - threshold = len(owners_after) - except TypeError: - threshold = None + owners_after = current + try: + threshold = len(owners_after) + except TypeError: + threshold = None if isinstance(owners_after, list) and len(owners_after) > 1: ffill = ThresholdSha256Fulfillment(threshold=threshold) @@ -764,7 +761,6 @@ class Transaction(object): # generate_conditions for owner_after in owners_after: - # TODO: Check types so this doesn't fail unpacking if not isinstance(owner_after, tuple) or len(owner_after) != 2: raise ValueError(('Each `owner_after` in the list is a tuple' ' of `([], )`')) @@ -821,21 +817,14 @@ class Transaction(object): raise ValueError('`inputs` must contain at least one item') if not isinstance(owners_after, list): raise TypeError('`owners_after` must be a list instance') - - # # NOTE: See doc strings `Note` for description. - # if len(inputs) == len(owners_after): - # if len(owners_after) == 1: - # conditions = [Condition.generate(owners_after)] - # elif len(owners_after) > 1: - # conditions = [Condition.generate(owners) for owners - # in owners_after] - # else: - # # TODO: Why?? - # raise ValueError("`inputs` and `owners_after`'s count must be the " - # "same") + if len(owners_after) == 0: + raise ValueError('`owners_after` list cannot be empty') conds = [] for owner_after in owners_after: + if not isinstance(owner_after, tuple) or len(owner_after) != 2: + raise ValueError(('Each `owner_after` in the list is a tuple' + ' of `([], )`')) pub_keys, amount = owner_after conds.append(Condition.generate(pub_keys, amount)) diff --git a/bigchaindb/pipelines/vote.py b/bigchaindb/pipelines/vote.py index 6b12f55b..3d30de35 100644 --- a/bigchaindb/pipelines/vote.py +++ b/bigchaindb/pipelines/vote.py @@ -40,7 +40,7 @@ class Vote: self.validity = {} self.invalid_dummy_tx = Transaction.create([self.bigchain.me], - [self.bigchain.me]) + [([self.bigchain.me], 1)]) def validate_block(self, block): if not self.bigchain.has_previous_vote(block['id'], block['block']['voters']): diff --git a/tests/assets/test_divisible_assets.py b/tests/assets/test_divisible_assets.py index aeed19e6..d808008c 100644 --- a/tests/assets/test_divisible_assets.py +++ b/tests/assets/test_divisible_assets.py @@ -32,7 +32,8 @@ def test_single_in_single_own_multiple_out_single_own_create(b, user_vk): from bigchaindb.common.transaction import Asset asset = Asset(divisible=True) - tx = Transaction.create([b.me], [([user_vk], 50), ([user_vk], 50)], asset=asset) + tx = Transaction.create([b.me], [([user_vk], 50), ([user_vk], 50)], + asset=asset) tx_signed = tx.sign([b.me_private]) assert tx_signed.validate(b) == tx_signed @@ -161,7 +162,7 @@ def test_single_in_single_own_single_out_single_own_transfer(b, user_vk, # Single owners_after @pytest.mark.usefixtures('inputs') def test_single_in_single_own_multiple_out_single_own_transfer(b, user_vk, - user_sk): + user_sk): from bigchaindb.models import Transaction from bigchaindb.common.transaction import Asset @@ -197,7 +198,7 @@ def test_single_in_single_own_multiple_out_single_own_transfer(b, user_vk, # Multiple owners_after @pytest.mark.usefixtures('inputs') def test_single_in_single_own_single_out_multiple_own_transfer(b, user_vk, - user_sk): + user_sk): from bigchaindb.models import Transaction from bigchaindb.common.transaction import Asset @@ -238,7 +239,7 @@ def test_single_in_single_own_single_out_multiple_own_transfer(b, user_vk, # owners_after @pytest.mark.usefixtures('inputs') def test_single_in_single_own_multiple_out_mix_own_transfer(b, user_vk, - user_sk): + user_sk): from bigchaindb.models import Transaction from bigchaindb.common.transaction import Asset @@ -390,7 +391,6 @@ def test_multiple_in_multiple_own_single_out_single_own_transfer(b, user_vk, assert len(ffill_fid1['subfulfillments']) == 2 - # TRANSFER divisible asset # Multiple inputs # Mix: one input with a single owners_before, one input with multiple @@ -589,46 +589,15 @@ def test_amount_error_transfer(b, user_vk, user_sk): tx_transfer_signed.validate(b) -@pytest.mark.skip -@pytest.mark.usefixtures('inputs') -def test_transaction_unfulfilled_fulfillments(b, user_vk, - user_sk): - from bigchaindb.models import Transaction - from bigchaindb.common.transaction import Asset - - # CREATE divisible asset - asset = Asset(divisible=True) - tx_create = Transaction.create([b.me], - [([user_vk, b.me], 50), - ([user_vk, b.me], 50)], - 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 - tx_transfer = Transaction.transfer(tx_create.to_inputs(), [([b.me], 100)], - asset=tx_create.asset) - tx_transfer_signed = tx_transfer.sign([b.me_private, user_sk]) - - # TODO: This transaction has unfulfilled fulfillments and should be - # invalid. Somehow the validation passes - assert b.is_valid_transaction(tx_transfer_signed) == False - - -@pytest.mark.skip(reason=('get_subcondition_from_vk does not always work' - ' as expected')) +@pytest.mark.skip(reason='Figure out how to handle this case') @pytest.mark.usefixtures('inputs') def test_threshold_same_public_key(b, user_vk, user_sk): # If we try to fulfill a threshold condition where each subcondition has # the same key get_subcondition_from_vk will always return the first # subcondition. This means that only the 1st subfulfillment will be # generated + # Creating threshold conditions with the same key does not make sense but + # that does not mean that the code shouldn't work. from bigchaindb.models import Transaction from bigchaindb.common.transaction import Asset @@ -652,3 +621,66 @@ def test_threshold_same_public_key(b, user_vk, user_sk): tx_transfer_signed = tx_transfer.sign([user_sk, user_sk]) assert tx_transfer_signed.validate(b) == tx_transfer_signed + + +@pytest.mark.usefixtures('inputs') +def test_sum_amount(b, user_vk, user_sk): + from bigchaindb.models import Transaction + from bigchaindb.common.transaction import Asset + + # CREATE divisible asset with 3 outputs with amount 1 + asset = Asset(divisible=True) + tx_create = Transaction.create([b.me], + [([user_vk], 1), + ([user_vk], 1), + ([user_vk], 1)], + 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) + + # create a transfer transaction with one output and check if the amount + # is 3 + tx_transfer = Transaction.transfer(tx_create.to_inputs(), [([b.me], 3)], + asset=tx_create.asset) + tx_transfer_signed = tx_transfer.sign([user_sk]) + + assert tx_transfer_signed.validate(b) == tx_transfer_signed + assert len(tx_transfer_signed.conditions) == 1 + assert tx_transfer_signed.conditions[0].amount == 3 + + +@pytest.mark.usefixtures('inputs') +def test_divide(b, user_vk, user_sk): + from bigchaindb.models import Transaction + from bigchaindb.common.transaction import Asset + + # CREATE divisible asset with 1 output with amount 3 + asset = Asset(divisible=True) + tx_create = Transaction.create([b.me], [([user_vk], 3)], + 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) + + # create a transfer transaction with 3 outputs and check if the amount + # of each output is 1 + tx_transfer = Transaction.transfer(tx_create.to_inputs(), + [([b.me], 1), ([b.me], 1), ([b.me], 1)], + asset=tx_create.asset) + tx_transfer_signed = tx_transfer.sign([user_sk]) + + assert tx_transfer_signed.validate(b) == tx_transfer_signed + assert len(tx_transfer_signed.conditions) == 3 + for condition in tx_transfer_signed.conditions: + assert condition.amount == 1 diff --git a/tests/common/test_transaction.py b/tests/common/test_transaction.py index d5754b47..50e26c21 100644 --- a/tests/common/test_transaction.py +++ b/tests/common/test_transaction.py @@ -171,28 +171,6 @@ def test_generate_conditions_split_half_recursive(user_pub, user2_pub, assert cond.fulfillment.to_dict() == expected.to_dict() -def test_generate_conditions_split_half_recursive_custom_threshold(user_pub, - user2_pub, - user3_pub): - from bigchaindb.common.transaction import Condition - from cryptoconditions import Ed25519Fulfillment, ThresholdSha256Fulfillment - - expected_simple1 = Ed25519Fulfillment(public_key=user_pub) - expected_simple2 = Ed25519Fulfillment(public_key=user2_pub) - expected_simple3 = Ed25519Fulfillment(public_key=user3_pub) - - expected = ThresholdSha256Fulfillment(threshold=1) - expected.add_subfulfillment(expected_simple1) - expected_threshold = ThresholdSha256Fulfillment(threshold=1) - expected_threshold.add_subfulfillment(expected_simple2) - expected_threshold.add_subfulfillment(expected_simple3) - expected.add_subfulfillment(expected_threshold) - - cond = Condition.generate(([user_pub, ([user2_pub, expected_simple3], 1)], - 1), 1) - assert cond.fulfillment.to_dict() == expected.to_dict() - - def test_generate_conditions_split_half_single_owner(user_pub, user2_pub, user3_pub): from bigchaindb.common.transaction import Condition @@ -614,24 +592,23 @@ def test_validate_fulfillment_with_invalid_parameters(utx): input_conditions) is False -@mark.skip(reason='Talk to @TimDaub') def test_validate_multiple_fulfillments(user_ffill, user_cond, user_priv): from copy import deepcopy from bigchaindb.common.crypto import SigningKey - from bigchaindb.common.transaction import Transaction, Asset + from bigchaindb.common.transaction import Transaction, Asset, Condition # TODO: Why is there a fulfillment in the conditions list - tx = Transaction(Transaction.CREATE, Asset(), + tx = Transaction(Transaction.CREATE, Asset(divisible=True), [user_ffill, deepcopy(user_ffill)], - [user_ffill, deepcopy(user_cond)]) + [user_cond, deepcopy(user_cond)]) expected_first = deepcopy(tx) expected_second = deepcopy(tx) expected_first.fulfillments = [expected_first.fulfillments[0]] - expected_first.conditions = [expected_first.conditions[0]] + expected_first.conditions = expected_first.conditions expected_second.fulfillments = [expected_second.fulfillments[1]] - expected_second.conditions = [expected_second.conditions[1]] + expected_second.conditions = expected_second.conditions expected_first_bytes = str(expected_first).encode() expected_first.fulfillments[0].fulfillment.sign(expected_first_bytes, @@ -700,7 +677,6 @@ def test_multiple_fulfillment_validation_of_transfer_tx(user_ffill, user_cond, assert transfer_tx.fulfillments_valid(tx.conditions) is True -@mark.skip(reason='Ask @TimDaub') def test_validate_fulfillments_of_transfer_tx_with_invalid_params(transfer_tx, cond_uri, utx, @@ -724,11 +700,6 @@ def test_validate_fulfillments_of_transfer_tx_with_invalid_params(transfer_tx, with raises(TypeError): transfer_tx.operation = "Operation that doesn't exist" transfer_tx.fulfillments_valid([utx.conditions[0]]) - # TODO: Why should this raise a ValueError? - with raises(ValueError): - tx = utx.sign([user_priv]) - tx.conditions = [] - tx.fulfillments_valid() def test_create_create_transaction_single_io(user_cond, user_pub, data, @@ -782,12 +753,15 @@ def test_validate_single_io_create_transaction(user_pub, user_priv, data): assert tx.fulfillments_valid() is True -@mark.skip(reason='Multiple inputs and outputs in CREATE not supported') -# TODO: Add digital assets def test_create_create_transaction_multiple_io(user_cond, user2_cond, user_pub, user2_pub): - from bigchaindb.common.transaction import Transaction + from bigchaindb.common.transaction import Transaction, Asset, Fulfillment + # a fulfillment for a create transaction with multiple `owners_before` + # is a fulfillment for an implicit threshold condition with + # weight = len(owners_before) + ffill = Fulfillment.generate([user_pub, user2_pub]).to_dict() + ffill.update({'fid': 0}) expected = { 'transaction': { 'conditions': [user_cond.to_dict(0), user2_cond.to_dict(1)], @@ -796,33 +770,20 @@ def test_create_create_transaction_multiple_io(user_cond, user2_cond, user_pub, 'message': 'hello' } }, - 'fulfillments': [ - { - 'owners_before': [ - user_pub, - ], - 'fid': 0, - 'fulfillment': None, - 'input': None - }, - { - 'owners_before': [ - user2_pub, - ], - 'fid': 1, - 'fulfillment': None, - 'input': None - } - ], + 'fulfillments': [ffill], 'operation': 'CREATE', }, 'version': 1 } - tx = Transaction.create([user_pub, user2_pub], [user_pub, user2_pub], - {'message': 'hello'}).to_dict() + asset = Asset(divisible=True) + tx = Transaction.create([user_pub, user2_pub], + [([user_pub], 1), ([user2_pub], 1)], + asset=asset, + metadata={'message': 'hello'}).to_dict() tx.pop('id') tx['transaction']['metadata'].pop('id') tx['transaction'].pop('timestamp') + tx['transaction'].pop('asset') assert tx == expected @@ -951,21 +912,21 @@ def test_validate_hashlock_create_transaction(user_pub, user_priv, data): assert tx.fulfillments_valid() is True -def test_create_create_transaction_with_invalid_parameters(): +def test_create_create_transaction_with_invalid_parameters(user_pub): from bigchaindb.common.transaction import Transaction with raises(TypeError): Transaction.create('not a list') with raises(TypeError): Transaction.create([], 'not a list') - with raises(NotImplementedError): - Transaction.create(['a', 'b'], ['c', 'd']) - with raises(NotImplementedError): - Transaction.create(['a'], [], time_expire=123) with raises(ValueError): - Transaction.create(['a'], [], secret=None) + Transaction.create([],[user_pub]) with raises(ValueError): - Transaction.create([], [], secret='wow, much secret') + Transaction.create([user_pub],[]) + with raises(ValueError): + Transaction.create([user_pub], [user_pub]) + with raises(ValueError): + Transaction.create([user_pub], [([user_pub],)]) def test_conditions_to_inputs(tx): @@ -1030,16 +991,15 @@ def test_create_transfer_transaction_single_io(tx, user_pub, user2_pub, assert transfer_tx.fulfillments_valid([tx.conditions[0]]) is True -@mark.skip(reason='FIXME: When divisible assets land') def test_create_transfer_transaction_multiple_io(user_pub, user_priv, user2_pub, user2_priv, user3_pub, user2_cond): - from bigchaindb.common.transaction import Transaction + from bigchaindb.common.transaction import Transaction, Asset - tx1 = Transaction.create([user_pub], [user_pub], {'message': 'hello'}) - tx1 = tx1.sign([user_priv]) - tx2 = Transaction.create([user2_pub], [user2_pub], {'message': 'hello'}) - tx2 = tx2.sign([user2_priv]) + asset = Asset(divisible=True) + tx = Transaction.create([user_pub], [([user_pub], 1), ([user2_pub], 1)], + asset=asset, metadata={'message': 'hello'}) + tx = tx.sign([user_priv]) expected = { 'transaction': { @@ -1053,7 +1013,7 @@ def test_create_transfer_transaction_multiple_io(user_pub, user_priv, 'fid': 0, 'fulfillment': None, 'input': { - 'txid': tx1.id, + 'txid': tx.id, 'cid': 0 } }, { @@ -1063,8 +1023,8 @@ def test_create_transfer_transaction_multiple_io(user_pub, user_priv, 'fid': 1, 'fulfillment': None, 'input': { - 'txid': tx2.id, - 'cid': 0 + 'txid': tx.id, + 'cid': 1 } } ], @@ -1072,30 +1032,29 @@ def test_create_transfer_transaction_multiple_io(user_pub, user_priv, }, 'version': 1 } - tx1_inputs = tx1.to_inputs() - tx2_inputs = tx2.to_inputs() - tx_inputs = tx1_inputs + tx2_inputs + tx_inputs = tx.to_inputs() - transfer_tx = Transaction.transfer(tx_inputs, [[user2_pub], [user2_pub]]) + transfer_tx = Transaction.transfer(tx.to_inputs(), + [([user2_pub], 1), ([user2_pub], 1)], + asset=tx.asset) transfer_tx = transfer_tx.sign([user_priv, user2_priv]) - transfer_tx = transfer_tx assert len(transfer_tx.fulfillments) == 2 assert len(transfer_tx.conditions) == 2 - combined_conditions = tx1.conditions + tx2.conditions - assert transfer_tx.fulfillments_valid(combined_conditions) is True + assert transfer_tx.fulfillments_valid(tx.conditions) is True transfer_tx = transfer_tx.to_dict() transfer_tx['transaction']['fulfillments'][0]['fulfillment'] = None transfer_tx['transaction']['fulfillments'][1]['fulfillment'] = None transfer_tx['transaction'].pop('timestamp') transfer_tx.pop('id') + transfer_tx['transaction'].pop('asset') assert expected == transfer_tx -def test_create_transfer_with_invalid_parameters(): +def test_create_transfer_with_invalid_parameters(user_pub): from bigchaindb.common.transaction import Transaction, Asset with raises(TypeError): @@ -1106,6 +1065,10 @@ def test_create_transfer_with_invalid_parameters(): Transaction.transfer(['fulfillment'], {}, Asset()) with raises(ValueError): Transaction.transfer(['fulfillment'], [], Asset()) + with raises(ValueError): + Transaction.transfer(['fulfillment'], [user_pub], Asset()) + with raises(ValueError): + Transaction.transfer(['fulfillment'], [([user_pub],)], Asset()) def test_cant_add_empty_condition(): diff --git a/tests/conftest.py b/tests/conftest.py index 58178b7f..fcb6b318 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -72,7 +72,7 @@ def b(request, node_config): @pytest.fixture def create_tx(b, user_vk): from bigchaindb.models import Transaction - return Transaction.create([b.me], [user_vk]) + return Transaction.create([b.me], [([user_vk], 1)]) @pytest.fixture @@ -84,5 +84,5 @@ def signed_create_tx(b, create_tx): def signed_transfer_tx(signed_create_tx, user_vk, user_sk): from bigchaindb.models import Transaction inputs = signed_create_tx.to_inputs() - tx = Transaction.transfer(inputs, [user_vk], signed_create_tx.asset) + tx = Transaction.transfer(inputs, [([user_vk], 1)], signed_create_tx.asset) return tx.sign([user_sk]) diff --git a/tests/db/test_bigchain_api.py b/tests/db/test_bigchain_api.py index f0a88c44..74b875c7 100644 --- a/tests/db/test_bigchain_api.py +++ b/tests/db/test_bigchain_api.py @@ -15,7 +15,7 @@ def dummy_tx(): import bigchaindb from bigchaindb.models import Transaction b = bigchaindb.Bigchain() - tx = Transaction.create([b.me], [b.me]) + tx = Transaction.create([b.me], [([b.me], 1)]) tx = tx.sign([b.me_private]) return tx @@ -37,7 +37,7 @@ class TestBigchainApi(object): b.create_genesis_block() - tx = Transaction.create([b.me], [b.me]) + tx = Transaction.create([b.me], [([b.me], 1)]) tx = tx.sign([b.me_private]) monkeypatch.setattr('time.time', lambda: 1) block1 = b.create_block([tx]) @@ -60,7 +60,7 @@ class TestBigchainApi(object): b.create_genesis_block() - tx = Transaction.create([b.me], [b.me]) + tx = Transaction.create([b.me], [([b.me], 1)]) tx = tx.sign([b.me_private]) block1 = b.create_block([tx]) @@ -74,7 +74,7 @@ class TestBigchainApi(object): b.create_genesis_block() - tx = Transaction.create([b.me], [b.me]) + tx = Transaction.create([b.me], [([b.me], 1)]) tx = tx.sign([b.me_private]) monkeypatch.setattr('time.time', lambda: 1) @@ -99,7 +99,7 @@ class TestBigchainApi(object): b.create_genesis_block() - tx = Transaction.create([b.me], [b.me]) + tx = Transaction.create([b.me], [([b.me], 1)]) tx = tx.sign([b.me_private]) monkeypatch.setattr('time.time', lambda: 1) @@ -107,13 +107,15 @@ class TestBigchainApi(object): b.write_block(block1, durability='hard') monkeypatch.setattr('time.time', lambda: 2) - transfer_tx = Transaction.transfer(tx.to_inputs(), [b.me], tx.asset) + transfer_tx = Transaction.transfer(tx.to_inputs(), [([b.me], 1)], + tx.asset) transfer_tx = transfer_tx.sign([b.me_private]) block2 = b.create_block([transfer_tx]) b.write_block(block2, durability='hard') monkeypatch.setattr('time.time', lambda: 3) - transfer_tx2 = Transaction.transfer(tx.to_inputs(), [b.me], tx.asset) + transfer_tx2 = Transaction.transfer(tx.to_inputs(), [([b.me], 1)], + tx.asset) transfer_tx2 = transfer_tx2.sign([b.me_private]) block3 = b.create_block([transfer_tx2]) b.write_block(block3, durability='hard') @@ -133,7 +135,7 @@ class TestBigchainApi(object): b.create_genesis_block() - tx = Transaction.create([b.me], [b.me]) + tx = Transaction.create([b.me], [([b.me], 1)]) tx = tx.sign([b.me_private]) monkeypatch.setattr('time.time', lambda: 1) @@ -159,13 +161,13 @@ class TestBigchainApi(object): b.create_genesis_block() monkeypatch.setattr('time.time', lambda: 1) - tx1 = Transaction.create([b.me], [b.me]) + tx1 = Transaction.create([b.me], [([b.me], 1)]) tx1 = tx1.sign([b.me_private]) block1 = b.create_block([tx1]) b.write_block(block1, durability='hard') monkeypatch.setattr('time.time', lambda: 2) - tx2 = Transaction.create([b.me], [b.me]) + tx2 = Transaction.create([b.me], [([b.me], 1)]) tx2 = tx2.sign([b.me_private]) block2 = b.create_block([tx2]) b.write_block(block2, durability='hard') @@ -185,7 +187,7 @@ class TestBigchainApi(object): from bigchaindb.models import Transaction metadata = {'msg': 'Hello BigchainDB!'} - tx = Transaction.create([b.me], [user_vk], metadata=metadata) + tx = Transaction.create([b.me], [([user_vk], 1)], metadata=metadata) block = b.create_block([tx]) b.write_block(block, durability='hard') @@ -205,7 +207,7 @@ class TestBigchainApi(object): input_tx = b.get_owned_ids(user_vk).pop() input_tx = b.get_transaction(input_tx.txid) inputs = input_tx.to_inputs() - tx = Transaction.transfer(inputs, [user_vk], input_tx.asset) + tx = Transaction.transfer(inputs, [([user_vk], 1)], input_tx.asset) tx = tx.sign([user_sk]) response = b.write_transaction(tx) @@ -223,7 +225,7 @@ class TestBigchainApi(object): input_tx = b.get_owned_ids(user_vk).pop() input_tx = b.get_transaction(input_tx.txid) inputs = input_tx.to_inputs() - tx = Transaction.transfer(inputs, [user_vk], input_tx.asset) + tx = Transaction.transfer(inputs, [([user_vk], 1)], input_tx.asset) tx = tx.sign([user_sk]) b.write_transaction(tx) @@ -243,7 +245,7 @@ class TestBigchainApi(object): input_tx = b.get_owned_ids(user_vk).pop() input_tx = b.get_transaction(input_tx.txid) inputs = input_tx.to_inputs() - tx = Transaction.transfer(inputs, [user_vk], input_tx.asset) + tx = Transaction.transfer(inputs, [([user_vk], 1)], input_tx.asset) tx = tx.sign([user_sk]) b.write_transaction(tx) @@ -499,7 +501,7 @@ class TestBigchainApi(object): input_tx = b.get_owned_ids(user_vk).pop() input_tx = b.get_transaction(input_tx.txid) inputs = input_tx.to_inputs() - tx = Transaction.transfer(inputs, [user_vk], input_tx.asset) + tx = Transaction.transfer(inputs, [([user_vk], 1)], input_tx.asset) tx = tx.sign([user_sk]) b.write_transaction(tx) @@ -525,7 +527,7 @@ class TestBigchainApi(object): input_tx = b.get_owned_ids(user_vk).pop() input_tx = b.get_transaction(input_tx.txid) inputs = input_tx.to_inputs() - tx = Transaction.transfer(inputs, [user_vk], input_tx.asset) + tx = Transaction.transfer(inputs, [([user_vk], 1)], input_tx.asset) tx = tx.sign([user_sk]) b.write_transaction(tx) @@ -549,7 +551,7 @@ class TestBigchainApi(object): fulfillment = Fulfillment(Ed25519Fulfillment(public_key=user_vk), [user_vk], TransactionLink('somethingsomething', 0)) - tx = Transaction.transfer([fulfillment], [user_vk], Asset()) + tx = Transaction.transfer([fulfillment], [([user_vk], 1)], Asset()) with pytest.raises(TransactionDoesNotExist) as excinfo: tx.validate(Bigchain()) @@ -591,7 +593,7 @@ class TestTransactionValidation(object): 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 = Transaction.create([vk], [([user_vk], 1)]) tx.operation = 'TRANSFER' tx.asset = input_transaction.asset tx.fulfillments[0].tx_input = input_tx @@ -635,7 +637,7 @@ class TestTransactionValidation(object): input_tx = b.get_owned_ids(user_vk).pop() input_tx = b.get_transaction(input_tx.txid) inputs = input_tx.to_inputs() - transfer_tx = Transaction.transfer(inputs, [user_vk], input_tx.asset) + transfer_tx = Transaction.transfer(inputs, [([user_vk], 1)], input_tx.asset) transfer_tx = transfer_tx.sign([user_sk]) assert transfer_tx == b.validate_transaction(transfer_tx) @@ -659,7 +661,7 @@ class TestTransactionValidation(object): inputs = input_tx.to_inputs() # create a transaction that's valid but not in a voted valid block - transfer_tx = Transaction.transfer(inputs, [user_vk], input_tx.asset) + transfer_tx = Transaction.transfer(inputs, [([user_vk], 1)], input_tx.asset) transfer_tx = transfer_tx.sign([user_sk]) assert transfer_tx == b.validate_transaction(transfer_tx) @@ -669,8 +671,9 @@ class TestTransactionValidation(object): b.write_block(block, durability='hard') # create transaction with the undecided input - tx_invalid = Transaction.transfer(transfer_tx.to_inputs(), [user_vk], - transfer_tx.asset) + tx_invalid = Transaction.transfer(transfer_tx.to_inputs(), + [([user_vk], 1)], + transfer_tx.asset) tx_invalid = tx_invalid.sign([user_sk]) with pytest.raises(FulfillmentNotInValidBlock): @@ -768,7 +771,7 @@ class TestMultipleInputs(object): tx_link = b.get_owned_ids(user_vk).pop() input_tx = b.get_transaction(tx_link.txid) inputs = input_tx.to_inputs() - tx = Transaction.transfer(inputs, [user2_vk], input_tx.asset) + tx = Transaction.transfer(inputs, [([user2_vk], 1)], input_tx.asset) tx = tx.sign([user_sk]) # validate transaction @@ -776,69 +779,6 @@ class TestMultipleInputs(object): assert len(tx.fulfillments) == 1 assert len(tx.conditions) == 1 - @pytest.mark.skipif(reason=('Multiple inputs are only allowed for the ' - 'same asset. Remove this after implementing ', - 'multiple assets')) - @pytest.mark.usefixtures('inputs') - def test_transfer_single_owners_multiple_inputs(self, b, user_sk, user_vk): - from bigchaindb.common import crypto - from bigchaindb.models import Transaction - - user2_sk, user2_vk = crypto.generate_key_pair() - - # get inputs - owned_inputs = b.get_owned_ids(user_vk) - input_txs = [b.get_transaction(tx_link.txid) for tx_link - in owned_inputs] - inputs = sum([input_tx.to_inputs() for input_tx in input_txs], []) - tx = Transaction.transfer(inputs, len(inputs) * [[user_vk]]) - tx = tx.sign([user_sk]) - assert b.validate_transaction(tx) == tx - assert len(tx.fulfillments) == len(inputs) - assert len(tx.conditions) == len(inputs) - - @pytest.mark.skipif(reason=('Multiple inputs are only allowed for the ' - 'same asset. Remove this after implementing ', - 'multiple assets')) - @pytest.mark.usefixtures('inputs') - def test_transfer_single_owners_single_input_from_multiple_outputs(self, b, - user_sk, - user_vk): - from bigchaindb.common import crypto - from bigchaindb.models import Transaction - - user2_sk, user2_vk = crypto.generate_key_pair() - - # get inputs - owned_inputs = b.get_owned_ids(user_vk) - input_txs = [b.get_transaction(tx_link.txid) for tx_link - in owned_inputs] - inputs = sum([input_tx.to_inputs() for input_tx in input_txs], []) - tx = Transaction.transfer(inputs, len(inputs) * [[user2_vk]]) - tx = tx.sign([user_sk]) - - # create block with the transaction - block = b.create_block([tx]) - b.write_block(block, durability='hard') - - # vote block valid - vote = b.vote(block.id, b.get_last_voted_block().id, True) - b.write_vote(vote) - - # get inputs from user2 - owned_inputs = b.get_owned_ids(user2_vk) - assert len(owned_inputs) == len(inputs) - - # create a transaction with a single input from a multiple output transaction - tx_link = owned_inputs.pop() - inputs = b.get_transaction(tx_link.txid).to_inputs([0]) - tx = Transaction.transfer(inputs, [user_vk]) - tx = tx.sign([user2_sk]) - - assert b.is_valid_transaction(tx) == tx - assert len(tx.fulfillments) == 1 - assert len(tx.conditions) == 1 - def test_single_owner_before_multiple_owners_after_single_input(self, b, user_sk, user_vk, @@ -852,47 +792,14 @@ class TestMultipleInputs(object): owned_inputs = b.get_owned_ids(user_vk) tx_link = owned_inputs.pop() input_tx = b.get_transaction(tx_link.txid) - tx = Transaction.transfer(input_tx.to_inputs(), [[user2_vk, user3_vk]], input_tx.asset) + tx = Transaction.transfer(input_tx.to_inputs(), + [([user2_vk, user3_vk], 1)], input_tx.asset) tx = tx.sign([user_sk]) assert b.is_valid_transaction(tx) == tx assert len(tx.fulfillments) == 1 assert len(tx.conditions) == 1 - @pytest.mark.skipif(reason=('Multiple inputs are only allowed for the ' - 'same asset. Remove this after implementing ', - 'multiple assets')) - @pytest.mark.usefixtures('inputs') - def test_single_owner_before_multiple_owners_after_multiple_inputs(self, b, - user_sk, - user_vk): - from bigchaindb.common import crypto - from bigchaindb.models import Transaction - - user2_sk, user2_vk = crypto.generate_key_pair() - user3_sk, user3_vk = crypto.generate_key_pair() - - owned_inputs = b.get_owned_ids(user_vk) - input_txs = [b.get_transaction(tx_link.txid) for tx_link - in owned_inputs] - inputs = sum([input_tx.to_inputs() for input_tx in input_txs], []) - - tx = Transaction.transfer(inputs, len(inputs) * [[user2_vk, user3_vk]]) - tx = tx.sign([user_sk]) - - # create block with the transaction - block = b.create_block([tx]) - b.write_block(block, durability='hard') - - # vote block valid - vote = b.vote(block.id, b.get_last_voted_block().id, True) - b.write_vote(vote) - - # validate transaction - assert b.is_valid_transaction(tx) == tx - assert len(tx.fulfillments) == len(inputs) - assert len(tx.conditions) == len(inputs) - @pytest.mark.usefixtures('inputs') def test_multiple_owners_before_single_owner_after_single_input(self, b, user_sk, @@ -903,7 +810,7 @@ class TestMultipleInputs(object): user2_sk, user2_vk = crypto.generate_key_pair() user3_sk, user3_vk = crypto.generate_key_pair() - tx = Transaction.create([b.me], [user_vk, user2_vk]) + tx = Transaction.create([b.me], [([user_vk, user2_vk], 1)]) tx = tx.sign([b.me_private]) block = b.create_block([tx]) b.write_block(block, durability='hard') @@ -916,7 +823,8 @@ class TestMultipleInputs(object): input_tx = b.get_transaction(owned_input.txid) inputs = input_tx.to_inputs() - transfer_tx = Transaction.transfer(inputs, [user3_vk], input_tx.asset) + transfer_tx = Transaction.transfer(inputs, [([user3_vk], 1)], + input_tx.asset) transfer_tx = transfer_tx.sign([user_sk, user2_sk]) # validate transaction @@ -924,29 +832,6 @@ class TestMultipleInputs(object): assert len(transfer_tx.fulfillments) == 1 assert len(transfer_tx.conditions) == 1 - @pytest.mark.skipif(reason=('Multiple inputs are only allowed for the ' - 'same asset. Remove this after implementing ', - 'multiple assets')) - @pytest.mark.usefixtures('inputs_shared') - def test_multiple_owners_before_single_owner_after_multiple_inputs(self, b, - user_sk, user_vk, user2_vk, user2_sk): - from bigchaindb.common import crypto - from bigchaindb.models import Transaction - - # create a new users - user3_sk, user3_vk = crypto.generate_key_pair() - - tx_links = b.get_owned_ids(user_vk) - inputs = sum([b.get_transaction(tx_link.txid).to_inputs() for tx_link - in tx_links], []) - - tx = Transaction.transfer(inputs, len(inputs) * [[user3_vk]]) - tx = tx.sign([user_sk, user2_sk]) - - assert b.is_valid_transaction(tx) == tx - assert len(tx.fulfillments) == len(inputs) - assert len(tx.conditions) == len(inputs) - @pytest.mark.usefixtures('inputs') def test_multiple_owners_before_multiple_owners_after_single_input(self, b, user_sk, @@ -958,7 +843,7 @@ class TestMultipleInputs(object): user3_sk, user3_vk = crypto.generate_key_pair() user4_sk, user4_vk = crypto.generate_key_pair() - tx = Transaction.create([b.me], [user_vk, user2_vk]) + tx = Transaction.create([b.me], [([user_vk, user2_vk], 1)]) tx = tx.sign([b.me_private]) block = b.create_block([tx]) b.write_block(block, durability='hard') @@ -971,38 +856,14 @@ class TestMultipleInputs(object): tx_link = b.get_owned_ids(user_vk).pop() tx_input = b.get_transaction(tx_link.txid) - tx = Transaction.transfer(tx_input.to_inputs(), [[user3_vk, user4_vk]], tx_input.asset) + tx = Transaction.transfer(tx_input.to_inputs(), + [([user3_vk, user4_vk], 1)], tx_input.asset) tx = tx.sign([user_sk, user2_sk]) assert b.is_valid_transaction(tx) == tx assert len(tx.fulfillments) == 1 assert len(tx.conditions) == 1 - @pytest.mark.skipif(reason=('Multiple inputs are only allowed for the ' - 'same asset. Remove this after implementing ', - 'multiple assets')) - @pytest.mark.usefixtures('inputs_shared') - def test_multiple_owners_before_multiple_owners_after_multiple_inputs(self, b, - user_sk, user_vk, - user2_sk, user2_vk): - from bigchaindb.common import crypto - from bigchaindb.models import Transaction - - # create a new users - user3_sk, user3_vk = crypto.generate_key_pair() - user4_sk, user4_vk = crypto.generate_key_pair() - - tx_links = b.get_owned_ids(user_vk) - inputs = sum([b.get_transaction(tx_link.txid).to_inputs() for tx_link - in tx_links], []) - - tx = Transaction.transfer(inputs, len(inputs) * [[user3_vk, user4_vk]]) - tx = tx.sign([user_sk, user2_sk]) - - assert b.is_valid_transaction(tx) == tx - assert len(tx.fulfillments) == len(inputs) - assert len(tx.conditions) == len(inputs) - def test_get_owned_ids_single_tx_single_output(self, b, user_sk, user_vk): from bigchaindb.common import crypto from bigchaindb.common.transaction import TransactionLink @@ -1010,7 +871,7 @@ class TestMultipleInputs(object): user2_sk, user2_vk = crypto.generate_key_pair() - tx = Transaction.create([b.me], [user_vk]) + tx = Transaction.create([b.me], [([user_vk], 1)]) tx = tx.sign([b.me_private]) block = b.create_block([tx]) b.write_block(block, durability='hard') @@ -1020,7 +881,7 @@ class TestMultipleInputs(object): assert owned_inputs_user1 == [TransactionLink(tx.id, 0)] assert owned_inputs_user2 == [] - tx = Transaction.transfer(tx.to_inputs(), [user2_vk], tx.asset) + tx = Transaction.transfer(tx.to_inputs(), [([user2_vk], 1)], tx.asset) tx = tx.sign([user_sk]) block = b.create_block([tx]) b.write_block(block, durability='hard') @@ -1040,7 +901,7 @@ class TestMultipleInputs(object): genesis = b.create_genesis_block() user2_sk, user2_vk = crypto.generate_key_pair() - tx = Transaction.create([b.me], [user_vk]) + tx = Transaction.create([b.me], [([user_vk], 1)]) tx = tx.sign([b.me_private]) block = b.create_block([tx]) b.write_block(block, durability='hard') @@ -1056,7 +917,8 @@ class TestMultipleInputs(object): # NOTE: The transaction itself is valid, still will mark the block # as invalid to mock the behavior. - tx_invalid = Transaction.transfer(tx.to_inputs(), [user2_vk], tx.asset) + tx_invalid = Transaction.transfer(tx.to_inputs(), [([user2_vk], 1)], + tx.asset) tx_invalid = tx_invalid.sign([user_sk]) block = b.create_block([tx_invalid]) b.write_block(block, durability='hard') @@ -1072,47 +934,46 @@ class TestMultipleInputs(object): assert owned_inputs_user1 == [TransactionLink(tx.id, 0)] assert owned_inputs_user2 == [] - @pytest.mark.skipif(reason=('Multiple inputs are only allowed for the ' - 'same asset. Remove this after implementing ', - 'multiple assets')) def test_get_owned_ids_single_tx_multiple_outputs(self, b, user_sk, user_vk): import random from bigchaindb.common import crypto - from bigchaindb.common.transaction import TransactionLink + from bigchaindb.common.transaction import TransactionLink, Asset from bigchaindb.models import Transaction user2_sk, user2_vk = crypto.generate_key_pair() - transactions = [] - for i in range(2): - payload = {'somedata': random.randint(0, 255)} - tx = Transaction.create([b.me], [user_vk], payload) - tx = tx.sign([b.me_private]) - transactions.append(tx) - block = b.create_block(transactions) + # create divisible asset + asset = Asset(divisible=True) + tx_create = Transaction.create([b.me], + [([user_vk], 1), ([user_vk], 1)], + asset=asset) + tx_create_signed = tx_create.sign([b.me_private]) + block = b.create_block([tx_create_signed]) b.write_block(block, durability='hard') # get input owned_inputs_user1 = b.get_owned_ids(user_vk) owned_inputs_user2 = b.get_owned_ids(user2_vk) - expected_owned_inputs_user1 = [TransactionLink(tx.id, 0) for tx - in transactions] + expected_owned_inputs_user1 = [TransactionLink(tx_create.id, 0), + TransactionLink(tx_create.id, 1)] assert owned_inputs_user1 == expected_owned_inputs_user1 assert owned_inputs_user2 == [] - inputs = sum([tx.to_inputs() for tx in transactions], []) - tx = Transaction.transfer(inputs, len(inputs) * [[user2_vk]]) - tx = tx.sign([user_sk]) - block = b.create_block([tx]) + # transfer divisible asset divided in two outputs + tx_transfer = Transaction.transfer(tx_create.to_inputs(), + [([user2_vk], 1), ([user2_vk], 1)], + asset=tx_create.asset) + tx_transfer_signed = tx_transfer.sign([user_sk]) + block = b.create_block([tx_transfer_signed]) b.write_block(block, durability='hard') owned_inputs_user1 = b.get_owned_ids(user_vk) owned_inputs_user2 = b.get_owned_ids(user2_vk) assert owned_inputs_user1 == [] - assert owned_inputs_user2 == [TransactionLink(tx.id, 0), - TransactionLink(tx.id, 1)] + assert owned_inputs_user2 == [TransactionLink(tx_transfer.id, 0), + TransactionLink(tx_transfer.id, 1)] def test_get_owned_ids_multiple_owners(self, b, user_sk, user_vk): from bigchaindb.common import crypto @@ -1122,7 +983,7 @@ class TestMultipleInputs(object): user2_sk, user2_vk = crypto.generate_key_pair() user3_sk, user3_vk = crypto.generate_key_pair() - tx = Transaction.create([b.me], [user_vk, user2_vk]) + tx = Transaction.create([b.me], [([user_vk, user2_vk],1)]) tx = tx.sign([b.me_private]) block = b.create_block([tx]) b.write_block(block, durability='hard') @@ -1134,7 +995,7 @@ class TestMultipleInputs(object): assert owned_inputs_user1 == owned_inputs_user2 assert owned_inputs_user1 == expected_owned_inputs_user1 - tx = Transaction.transfer(tx.to_inputs(), [user3_vk], tx.asset) + tx = Transaction.transfer(tx.to_inputs(), [([user3_vk], 1)], tx.asset) tx = tx.sign([user_sk, user2_sk]) block = b.create_block([tx]) b.write_block(block, durability='hard') @@ -1150,7 +1011,7 @@ class TestMultipleInputs(object): user2_sk, user2_vk = crypto.generate_key_pair() - tx = Transaction.create([b.me], [user_vk]) + tx = Transaction.create([b.me], [([user_vk], 1)]) tx = tx.sign([b.me_private]) block = b.create_block([tx]) b.write_block(block, durability='hard') @@ -1164,7 +1025,7 @@ class TestMultipleInputs(object): assert spent_inputs_user1 is None # create a transaction and block - tx = Transaction.transfer(tx.to_inputs(), [user2_vk], tx.asset) + tx = Transaction.transfer(tx.to_inputs(), [([user2_vk], 1)], tx.asset) tx = tx.sign([user_sk]) block = b.create_block([tx]) b.write_block(block, durability='hard') @@ -1181,7 +1042,7 @@ class TestMultipleInputs(object): # create a new users user2_sk, user2_vk = crypto.generate_key_pair() - tx = Transaction.create([b.me], [user_vk]) + tx = Transaction.create([b.me], [([user_vk], 1)]) tx = tx.sign([b.me_private]) block = b.create_block([tx]) b.write_block(block, durability='hard') @@ -1199,7 +1060,7 @@ class TestMultipleInputs(object): assert spent_inputs_user1 is None # create a transaction and block - tx = Transaction.transfer(tx.to_inputs(), [user2_vk], tx.asset) + tx = Transaction.transfer(tx.to_inputs(), [([user2_vk], 1)], tx.asset) tx = tx.sign([user_sk]) block = b.create_block([tx]) b.write_block(block, durability='hard') @@ -1214,24 +1075,24 @@ class TestMultipleInputs(object): # Now there should be no spents (the block is invalid) assert spent_inputs_user1 is None - @pytest.mark.skipif(reason=('Multiple inputs are only allowed for the ' - 'same asset. Remove this after implementing ', - 'multiple assets')) def test_get_spent_single_tx_multiple_outputs(self, b, user_sk, user_vk): import random from bigchaindb.common import crypto from bigchaindb.models import Transaction + from bigchaindb.common.transaction import Asset # create a new users user2_sk, user2_vk = crypto.generate_key_pair() - transactions = [] - for i in range(3): - payload = {'somedata': random.randint(0, 255)} - tx = Transaction.create([b.me], [user_vk], payload) - tx = tx.sign([b.me_private]) - transactions.append(tx) - block = b.create_block(transactions) + # create a divisible asset with 3 outputs + asset = Asset(divisible=True) + tx_create = Transaction.create([b.me], + [([user_vk], 1), + ([user_vk], 1), + ([user_vk], 1)], + asset=asset) + tx_create_signed = tx_create.sign([b.me_private]) + block = b.create_block([tx_create_signed]) b.write_block(block, durability='hard') owned_inputs_user1 = b.get_owned_ids(user_vk) @@ -1240,22 +1101,22 @@ class TestMultipleInputs(object): for input_tx in owned_inputs_user1: assert b.get_spent(input_tx.txid, input_tx.cid) is None - # select inputs to use - inputs = sum([tx.to_inputs() for tx in transactions[:2]], []) - - # create a transaction and block - tx = Transaction.transfer(inputs, len(inputs) * [[user2_vk]]) - tx = tx.sign([user_sk]) - block = b.create_block([tx]) + # transfer the first 2 inputs + tx_transfer = Transaction.transfer(tx_create.to_inputs()[:2], + [([user2_vk], 1), ([user2_vk], 1)], + asset=tx_create.asset) + tx_transfer_signed = tx_transfer.sign([user_sk]) + block = b.create_block([tx_transfer_signed]) b.write_block(block, durability='hard') # check that used inputs are marked as spent - for ffill in inputs: - assert b.get_spent(ffill.tx_input.txid, ffill.tx_input.cid) == tx + for ffill in tx_create.to_inputs()[:2]: + spent_tx = b.get_spent(ffill.tx_input.txid, ffill.tx_input.cid) + assert spent_tx == tx_transfer_signed # check if remaining transaction that was unspent is also perceived # spendable by BigchainDB - assert b.get_spent(transactions[2].id, 0) is None + assert b.get_spent(tx_create.to_inputs()[2].tx_input.txid, 2) is None def test_get_spent_multiple_owners(self, b, user_sk, user_vk): import random @@ -1268,7 +1129,7 @@ class TestMultipleInputs(object): transactions = [] for i in range(3): payload = {'somedata': random.randint(0, 255)} - tx = Transaction.create([b.me], [user_vk, user2_vk], payload) + tx = Transaction.create([b.me], [([user_vk, user2_vk], 1)], payload) tx = tx.sign([b.me_private]) transactions.append(tx) block = b.create_block(transactions) @@ -1281,7 +1142,8 @@ class TestMultipleInputs(object): assert b.get_spent(input_tx.txid, input_tx.cid) is None # create a transaction - tx = Transaction.transfer(transactions[0].to_inputs(), [user3_vk], transactions[0].asset) + tx = Transaction.transfer(transactions[0].to_inputs(), + [([user3_vk], 1)], transactions[0].asset) tx = tx.sign([user_sk, user2_sk]) block = b.create_block([tx]) b.write_block(block, durability='hard') diff --git a/tests/pipelines/test_block_creation.py b/tests/pipelines/test_block_creation.py index c2403a08..5396ebc3 100644 --- a/tests/pipelines/test_block_creation.py +++ b/tests/pipelines/test_block_creation.py @@ -45,7 +45,7 @@ def test_create_block(b, user_vk): block_maker = BlockPipeline() for i in range(100): - tx = Transaction.create([b.me], [user_vk]) + tx = Transaction.create([b.me], [([user_vk], 1)]) tx = tx.sign([b.me_private]) block_maker.create(tx) @@ -63,7 +63,7 @@ def test_write_block(b, user_vk): txs = [] for i in range(100): - tx = Transaction.create([b.me], [user_vk]) + tx = Transaction.create([b.me], [([user_vk], 1)]) tx = tx.sign([b.me_private]) txs.append(tx) @@ -82,7 +82,7 @@ def test_duplicate_transaction(b, user_vk): txs = [] for i in range(10): - tx = Transaction.create([b.me], [user_vk]) + tx = Transaction.create([b.me], [([user_vk], 1)]) tx = tx.sign([b.me_private]) txs.append(tx) @@ -110,7 +110,7 @@ def test_delete_tx(b, user_vk): from bigchaindb.pipelines.block import BlockPipeline block_maker = BlockPipeline() for i in range(100): - tx = Transaction.create([b.me], [user_vk]) + tx = Transaction.create([b.me], [([user_vk], 1)]) tx = tx.sign([b.me_private]) block_maker.create(tx) # make sure the tx appears in the backlog @@ -139,7 +139,8 @@ def test_prefeed(b, user_vk): from bigchaindb.pipelines.block import initial for i in range(100): - tx = Transaction.create([b.me], [user_vk], {'msg': random.random()}) + tx = Transaction.create([b.me], [([user_vk], 1)], + {'msg': random.random()}) tx = tx.sign([b.me_private]) b.write_transaction(tx) @@ -168,7 +169,8 @@ def test_full_pipeline(b, user_vk): count_assigned_to_me = 0 for i in range(100): - tx = Transaction.create([b.me], [user_vk], {'msg': random.random()}) + tx = Transaction.create([b.me], [([user_vk], 1)], + {'msg': random.random()}) tx = tx.sign([b.me_private]).to_dict() assignee = random.choice([b.me, 'aaa', 'bbb', 'ccc']) tx['assignee'] = assignee diff --git a/tests/pipelines/test_election.py b/tests/pipelines/test_election.py index 669a75cb..d62869f5 100644 --- a/tests/pipelines/test_election.py +++ b/tests/pipelines/test_election.py @@ -15,7 +15,7 @@ def test_check_for_quorum_invalid(b, user_vk): e = election.Election() # create blocks with transactions - tx1 = Transaction.create([b.me], [user_vk]) + tx1 = Transaction.create([b.me], [([user_vk], 1)]) test_block = b.create_block([tx1]) # simulate a federation with four voters @@ -44,7 +44,7 @@ def test_check_for_quorum_invalid_prev_node(b, user_vk): e = election.Election() # create blocks with transactions - tx1 = Transaction.create([b.me], [user_vk]) + tx1 = Transaction.create([b.me], [([user_vk], 1)]) test_block = b.create_block([tx1]) # simulate a federation with four voters @@ -74,7 +74,7 @@ def test_check_for_quorum_valid(b, user_vk): e = election.Election() # create blocks with transactions - tx1 = Transaction.create([b.me], [user_vk]) + tx1 = Transaction.create([b.me], [([user_vk], 1)]) test_block = b.create_block([tx1]) # simulate a federation with four voters @@ -103,7 +103,7 @@ def test_check_requeue_transaction(b, user_vk): e = election.Election() # create blocks with transactions - tx1 = Transaction.create([b.me], [user_vk]) + tx1 = Transaction.create([b.me], [([user_vk], 1)]) test_block = b.create_block([tx1]) e.requeue_transactions(test_block) @@ -131,7 +131,8 @@ def test_full_pipeline(b, user_vk): # write two blocks txs = [] for i in range(100): - tx = Transaction.create([b.me], [user_vk], {'msg': random.random()}) + tx = Transaction.create([b.me], [([user_vk], 1)], + {'msg': random.random()}) tx = tx.sign([b.me_private]) txs.append(tx) @@ -140,7 +141,8 @@ def test_full_pipeline(b, user_vk): txs = [] for i in range(100): - tx = Transaction.create([b.me], [user_vk], {'msg': random.random()}) + tx = Transaction.create([b.me], [([user_vk], 1)], + {'msg': random.random()}) tx = tx.sign([b.me_private]) txs.append(tx) diff --git a/tests/pipelines/test_stale_monitor.py b/tests/pipelines/test_stale_monitor.py index 3a3e6ffe..95f298c5 100644 --- a/tests/pipelines/test_stale_monitor.py +++ b/tests/pipelines/test_stale_monitor.py @@ -10,7 +10,7 @@ import os def test_get_stale(b, user_vk): from bigchaindb.models import Transaction - tx = Transaction.create([b.me], [user_vk]) + tx = Transaction.create([b.me], [([user_vk], 1)]) tx = tx.sign([b.me_private]) b.write_transaction(tx, durability='hard') @@ -27,7 +27,7 @@ def test_get_stale(b, user_vk): def test_reassign_transactions(b, user_vk): from bigchaindb.models import Transaction # test with single node - tx = Transaction.create([b.me], [user_vk]) + tx = Transaction.create([b.me], [([user_vk], 1)]) tx = tx.sign([b.me_private]) b.write_transaction(tx, durability='hard') @@ -36,7 +36,7 @@ def test_reassign_transactions(b, user_vk): stm.reassign_transactions(tx.to_dict()) # test with federation - tx = Transaction.create([b.me], [user_vk]) + tx = Transaction.create([b.me], [([user_vk], 1)]) tx = tx.sign([b.me_private]) b.write_transaction(tx, durability='hard') @@ -51,7 +51,7 @@ def test_reassign_transactions(b, user_vk): assert reassigned_tx['assignee'] != tx['assignee'] # test with node not in federation - tx = Transaction.create([b.me], [user_vk]) + tx = Transaction.create([b.me], [([user_vk], 1)]) tx = tx.sign([b.me_private]).to_dict() tx.update({'assignee': 'lol'}) tx.update({'assignment_timestamp': time.time()}) @@ -85,7 +85,7 @@ def test_full_pipeline(monkeypatch, user_vk): monkeypatch.setattr('time.time', lambda: 1) for i in range(100): - tx = Transaction.create([b.me], [user_vk]) + tx = Transaction.create([b.me], [([user_vk], 1)]) tx = tx.sign([b.me_private]) original_txc.append(tx.to_dict()) diff --git a/tests/pipelines/test_vote.py b/tests/pipelines/test_vote.py index 5bd0eb52..ec2f6204 100644 --- a/tests/pipelines/test_vote.py +++ b/tests/pipelines/test_vote.py @@ -8,7 +8,7 @@ from multipipes import Pipe, Pipeline def dummy_tx(b): from bigchaindb.models import Transaction - tx = Transaction.create([b.me], [b.me]) + tx = Transaction.create([b.me], [([b.me], 1)]) tx = tx.sign([b.me_private]) return tx @@ -130,7 +130,7 @@ def test_vote_validate_transaction(b): assert validation == (True, 123, 1) # NOTE: Submit unsigned transaction to `validate_tx` yields `False`. - tx = Transaction.create([b.me], [b.me]) + tx = Transaction.create([b.me], [([b.me], 1)]) validation = vote_obj.validate_tx(tx, 456, 10) assert validation == (False, 456, 10) @@ -224,7 +224,7 @@ def test_valid_block_voting_with_create_transaction(b, monkeypatch): # create a `CREATE` transaction test_user_priv, test_user_pub = crypto.generate_key_pair() - tx = Transaction.create([b.me], [test_user_pub]) + tx = Transaction.create([b.me], [([test_user_pub], 1)]) tx = tx.sign([b.me_private]) monkeypatch.setattr('time.time', lambda: 1) @@ -265,7 +265,7 @@ def test_valid_block_voting_with_transfer_transactions(monkeypatch, b): # create a `CREATE` transaction test_user_priv, test_user_pub = crypto.generate_key_pair() - tx = Transaction.create([b.me], [test_user_pub]) + tx = Transaction.create([b.me], [([test_user_pub], 1)]) tx = tx.sign([b.me_private]) monkeypatch.setattr('time.time', lambda: 1) @@ -274,7 +274,8 @@ def test_valid_block_voting_with_transfer_transactions(monkeypatch, b): # create a `TRANSFER` transaction test_user2_priv, test_user2_pub = crypto.generate_key_pair() - tx2 = Transaction.transfer(tx.to_inputs(), [test_user2_pub], tx.asset) + tx2 = Transaction.transfer(tx.to_inputs(), [([test_user2_pub], 1)], + tx.asset) tx2 = tx2.sign([test_user_priv]) monkeypatch.setattr('time.time', lambda: 2) @@ -338,7 +339,7 @@ def test_unsigned_tx_in_block_voting(monkeypatch, b, user_vk): vote_pipeline.setup(indata=inpipe, outdata=outpipe) # NOTE: `tx` is invalid, because it wasn't signed. - tx = Transaction.create([b.me], [b.me]) + tx = Transaction.create([b.me], [([b.me], 1)]) block = b.create_block([tx]) inpipe.put(block.to_dict()) @@ -375,7 +376,7 @@ def test_invalid_id_tx_in_block_voting(monkeypatch, b, user_vk): vote_pipeline.setup(indata=inpipe, outdata=outpipe) # NOTE: `tx` is invalid, because its id is not corresponding to its content - tx = Transaction.create([b.me], [b.me]) + tx = Transaction.create([b.me], [([b.me], 1)]) tx = tx.sign([b.me_private]) block = b.create_block([tx]).to_dict() block['block']['transactions'][0]['id'] = 'an invalid tx id' @@ -414,7 +415,7 @@ def test_invalid_content_in_tx_in_block_voting(monkeypatch, b, user_vk): vote_pipeline.setup(indata=inpipe, outdata=outpipe) # NOTE: `tx` is invalid, because its content is not corresponding to its id - tx = Transaction.create([b.me], [b.me]) + tx = Transaction.create([b.me], [([b.me], 1)]) tx = tx.sign([b.me_private]) block = b.create_block([tx]).to_dict() block['block']['transactions'][0]['id'] = 'an invalid tx id' diff --git a/tests/web/test_basic_views.py b/tests/web/test_basic_views.py index 00e40a37..7d382ca5 100644 --- a/tests/web/test_basic_views.py +++ b/tests/web/test_basic_views.py @@ -36,7 +36,7 @@ def test_post_create_transaction_endpoint(b, client): from bigchaindb.models import Transaction user_priv, user_pub = crypto.generate_key_pair() - tx = Transaction.create([user_pub], [user_pub]) + tx = Transaction.create([user_pub], [([user_pub], 1)]) tx = tx.sign([user_priv]) res = client.post(TX_ENDPOINT, data=json.dumps(tx.to_dict())) @@ -48,7 +48,7 @@ def test_post_create_transaction_with_invalid_id(b, client): from bigchaindb.models import Transaction user_priv, user_pub = crypto.generate_key_pair() - tx = Transaction.create([user_pub], [user_pub]) + tx = Transaction.create([user_pub], [([user_pub], 1)]) tx = tx.sign([user_priv]).to_dict() tx['id'] = 'invalid id' @@ -60,7 +60,7 @@ def test_post_create_transaction_with_invalid_signature(b, client): from bigchaindb.models import Transaction user_priv, user_pub = crypto.generate_key_pair() - tx = Transaction.create([user_pub], [user_pub]) + tx = Transaction.create([user_pub], [([user_pub], 1)]) tx = tx.sign([user_priv]).to_dict() tx['transaction']['fulfillments'][0]['fulfillment'] = 'invalid signature' @@ -77,7 +77,8 @@ def test_post_transfer_transaction_endpoint(b, client, user_vk, user_sk): input_valid = b.get_owned_ids(user_vk).pop() create_tx = b.get_transaction(input_valid.txid) - transfer_tx = Transaction.transfer(create_tx.to_inputs(), [user_pub], create_tx.asset) + transfer_tx = Transaction.transfer(create_tx.to_inputs(), + [([user_pub], 1)], create_tx.asset) transfer_tx = transfer_tx.sign([user_sk]) res = client.post(TX_ENDPOINT, data=json.dumps(transfer_tx.to_dict())) @@ -94,7 +95,8 @@ def test_post_invalid_transfer_transaction_returns_400(b, client, user_vk, user_ input_valid = b.get_owned_ids(user_vk).pop() create_tx = b.get_transaction(input_valid.txid) - transfer_tx = Transaction.transfer(create_tx.to_inputs(), [user_pub], create_tx.asset) + transfer_tx = Transaction.transfer(create_tx.to_inputs(), + [([user_pub], 1)], create_tx.asset) res = client.post(TX_ENDPOINT, data=json.dumps(transfer_tx.to_dict())) assert res.status_code == 400 From 7313cd944159c8a7111e4283a98847e229378706 Mon Sep 17 00:00:00 2001 From: Rodolphe Marques Date: Wed, 9 Nov 2016 14:03:34 +0100 Subject: [PATCH 14/54] get_asset_by_id now uses the new db api --- bigchaindb/core.py | 12 +----------- bigchaindb/db/backends/rethinkdb.py | 19 +++++++++++++++++++ tests/db/test_bigchain_api.py | 2 +- 3 files changed, 21 insertions(+), 12 deletions(-) diff --git a/bigchaindb/core.py b/bigchaindb/core.py index 96824e3b..430b95d7 100644 --- a/bigchaindb/core.py +++ b/bigchaindb/core.py @@ -353,21 +353,11 @@ class Bigchain(object): :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 = self.backend.get_asset_by_id(asset_id) 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/db/backends/rethinkdb.py b/bigchaindb/db/backends/rethinkdb.py index 22937dd2..8f28eca9 100644 --- a/bigchaindb/db/backends/rethinkdb.py +++ b/bigchaindb/db/backends/rethinkdb.py @@ -180,6 +180,25 @@ class RethinkDBBackend: .concat_map(lambda block: block['block']['transactions']) .filter(lambda transaction: transaction['transaction']['asset']['id'] == asset_id)) + def get_asset_by_id(self, asset_id): + """Returns the asset associated with an asset_id + + Args: + asset_id (str): The asset id + + Returns: + Returns a rethinkdb cursor + """ + return 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'})) + def get_spent(self, transaction_id, condition_id): """Check if a `txid` was already used as an input. diff --git a/tests/db/test_bigchain_api.py b/tests/db/test_bigchain_api.py index 82f3442b..a7b4413d 100644 --- a/tests/db/test_bigchain_api.py +++ b/tests/db/test_bigchain_api.py @@ -269,7 +269,7 @@ class TestBigchainApi(object): input_tx = b.get_owned_ids(user_vk).pop() input_tx = b.get_transaction(input_tx.txid) inputs = input_tx.to_inputs() - tx = Transaction.transfer(inputs, [user_vk], input_tx.asset) + tx = Transaction.transfer(inputs, [([user_vk], 1)], input_tx.asset) tx = tx.sign([user_sk]) # Make sure there's a copy of tx in the backlog From dccbc3c1fe47835b59ded6f0e43e1bce8433da89 Mon Sep 17 00:00:00 2001 From: Rodolphe Marques Date: Wed, 9 Nov 2016 14:25:42 +0100 Subject: [PATCH 15/54] pep8 fixes --- bigchaindb/common/transaction.py | 11 +---------- bigchaindb/core.py | 4 ++-- bigchaindb/db/backends/rethinkdb.py | 3 ++- bigchaindb/pipelines/vote.py | 3 ++- tests/common/test_transaction.py | 9 ++++----- tests/db/test_bigchain_api.py | 19 ++++++++++--------- 6 files changed, 21 insertions(+), 28 deletions(-) diff --git a/bigchaindb/common/transaction.py b/bigchaindb/common/transaction.py index f87d9a3c..8a8fcded 100644 --- a/bigchaindb/common/transaction.py +++ b/bigchaindb/common/transaction.py @@ -3,8 +3,7 @@ from functools import reduce from uuid import uuid4 from cryptoconditions import (Fulfillment as CCFulfillment, - ThresholdSha256Fulfillment, Ed25519Fulfillment, - PreimageSha256Fulfillment) + ThresholdSha256Fulfillment, Ed25519Fulfillment) from cryptoconditions.exceptions import ParsingError from bigchaindb.common.crypto import SigningKey, hash_data @@ -800,7 +799,6 @@ class Transaction(object): pub_keys, amount = owner_after conds.append(Condition.generate(pub_keys, amount)) - metadata = Metadata(metadata) inputs = deepcopy(inputs) return cls(cls.TRANSFER, asset, inputs, conds, metadata) @@ -914,12 +912,6 @@ class Transaction(object): key_pairs = {gen_public_key(SigningKey(private_key)): SigningKey(private_key) for private_key in private_keys} - # TODO: What does the conditions of this transaction have to do with the - # fulfillments, and why does this enforce for the number of fulfillments - # and conditions to be the same? - # TODO: Need to check how this was done before common but I from what I remember we - # included the condition that we were fulfilling in the message to be signed. - # zippedIO = enumerate(zip(self.fulfillments, self.conditions)) for index, fulfillment in enumerate(self.fulfillments): # NOTE: We clone the current transaction but only add the condition # and fulfillment we're currently working on plus all @@ -1082,7 +1074,6 @@ class Transaction(object): """ input_condition_uris_count = len(input_condition_uris) fulfillments_count = len(self.fulfillments) - conditions_count = len(self.conditions) def gen_tx(fulfillment, condition, input_condition_uri=None): """Splits multiple IO Transactions into partial single IO diff --git a/bigchaindb/core.py b/bigchaindb/core.py index 430b95d7..3c4f5347 100644 --- a/bigchaindb/core.py +++ b/bigchaindb/core.py @@ -182,7 +182,8 @@ class Bigchain(object): try: return self.validate_transaction(transaction) - except (ValueError, exceptions.OperationError, exceptions.TransactionDoesNotExist, + except (ValueError, exceptions.OperationError, + exceptions.TransactionDoesNotExist, exceptions.TransactionOwnerError, exceptions.DoubleSpend, exceptions.InvalidHash, exceptions.InvalidSignature, exceptions.FulfillmentNotInValidBlock, exceptions.AmountError): @@ -358,7 +359,6 @@ class Bigchain(object): if cursor: return Asset.from_dict(cursor[0]['transaction']['asset']) - def get_spent(self, txid, cid): """Check if a `txid` was already used as an input. diff --git a/bigchaindb/db/backends/rethinkdb.py b/bigchaindb/db/backends/rethinkdb.py index 8f28eca9..944d5e7c 100644 --- a/bigchaindb/db/backends/rethinkdb.py +++ b/bigchaindb/db/backends/rethinkdb.py @@ -178,7 +178,8 @@ class RethinkDBBackend: 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']['asset']['id'] == asset_id)) def get_asset_by_id(self, asset_id): """Returns the asset associated with an asset_id diff --git a/bigchaindb/pipelines/vote.py b/bigchaindb/pipelines/vote.py index 3d30de35..b89e0786 100644 --- a/bigchaindb/pipelines/vote.py +++ b/bigchaindb/pipelines/vote.py @@ -43,7 +43,8 @@ class Vote: [([self.bigchain.me], 1)]) def validate_block(self, block): - if not self.bigchain.has_previous_vote(block['id'], block['block']['voters']): + if not self.bigchain.has_previous_vote(block['id'], + block['block']['voters']): try: block = Block.from_dict(block) except (exceptions.InvalidHash, exceptions.InvalidSignature): diff --git a/tests/common/test_transaction.py b/tests/common/test_transaction.py index 50e26c21..438c796c 100644 --- a/tests/common/test_transaction.py +++ b/tests/common/test_transaction.py @@ -596,7 +596,7 @@ def test_validate_multiple_fulfillments(user_ffill, user_cond, user_priv): from copy import deepcopy from bigchaindb.common.crypto import SigningKey - from bigchaindb.common.transaction import Transaction, Asset, Condition + from bigchaindb.common.transaction import Transaction, Asset # TODO: Why is there a fulfillment in the conditions list tx = Transaction(Transaction.CREATE, Asset(divisible=True), @@ -920,9 +920,9 @@ def test_create_create_transaction_with_invalid_parameters(user_pub): with raises(TypeError): Transaction.create([], 'not a list') with raises(ValueError): - Transaction.create([],[user_pub]) + Transaction.create([], [user_pub]) with raises(ValueError): - Transaction.create([user_pub],[]) + Transaction.create([user_pub], []) with raises(ValueError): Transaction.create([user_pub], [user_pub]) with raises(ValueError): @@ -998,7 +998,7 @@ def test_create_transfer_transaction_multiple_io(user_pub, user_priv, asset = Asset(divisible=True) tx = Transaction.create([user_pub], [([user_pub], 1), ([user2_pub], 1)], - asset=asset, metadata={'message': 'hello'}) + asset=asset, metadata={'message': 'hello'}) tx = tx.sign([user_priv]) expected = { @@ -1032,7 +1032,6 @@ def test_create_transfer_transaction_multiple_io(user_pub, user_priv, }, 'version': 1 } - tx_inputs = tx.to_inputs() transfer_tx = Transaction.transfer(tx.to_inputs(), [([user2_pub], 1), ([user2_pub], 1)], diff --git a/tests/db/test_bigchain_api.py b/tests/db/test_bigchain_api.py index a7b4413d..31844d29 100644 --- a/tests/db/test_bigchain_api.py +++ b/tests/db/test_bigchain_api.py @@ -582,7 +582,7 @@ class TestBigchainApi(object): TransactionLink('somethingsomething', 0)) tx = Transaction.transfer([fulfillment], [([user_vk], 1)], Asset()) - with pytest.raises(TransactionDoesNotExist) as excinfo: + with pytest.raises(TransactionDoesNotExist): tx.validate(Bigchain()) @@ -666,7 +666,8 @@ class TestTransactionValidation(object): input_tx = b.get_owned_ids(user_vk).pop() input_tx = b.get_transaction(input_tx.txid) inputs = input_tx.to_inputs() - transfer_tx = Transaction.transfer(inputs, [([user_vk], 1)], input_tx.asset) + transfer_tx = Transaction.transfer(inputs, [([user_vk], 1)], + input_tx.asset) transfer_tx = transfer_tx.sign([user_sk]) assert transfer_tx == b.validate_transaction(transfer_tx) @@ -690,7 +691,8 @@ class TestTransactionValidation(object): inputs = input_tx.to_inputs() # create a transaction that's valid but not in a voted valid block - transfer_tx = Transaction.transfer(inputs, [([user_vk], 1)], input_tx.asset) + transfer_tx = Transaction.transfer(inputs, [([user_vk], 1)], + input_tx.asset) transfer_tx = transfer_tx.sign([user_sk]) assert transfer_tx == b.validate_transaction(transfer_tx) @@ -965,7 +967,6 @@ class TestMultipleInputs(object): def test_get_owned_ids_single_tx_multiple_outputs(self, b, user_sk, user_vk): - import random from bigchaindb.common import crypto from bigchaindb.common.transaction import TransactionLink, Asset from bigchaindb.models import Transaction @@ -992,8 +993,8 @@ class TestMultipleInputs(object): # transfer divisible asset divided in two outputs tx_transfer = Transaction.transfer(tx_create.to_inputs(), - [([user2_vk], 1), ([user2_vk], 1)], - asset=tx_create.asset) + [([user2_vk], 1), ([user2_vk], 1)], + asset=tx_create.asset) tx_transfer_signed = tx_transfer.sign([user_sk]) block = b.create_block([tx_transfer_signed]) b.write_block(block, durability='hard') @@ -1012,7 +1013,7 @@ class TestMultipleInputs(object): user2_sk, user2_vk = crypto.generate_key_pair() user3_sk, user3_vk = crypto.generate_key_pair() - tx = Transaction.create([b.me], [([user_vk, user2_vk],1)]) + tx = Transaction.create([b.me], [([user_vk, user2_vk], 1)]) tx = tx.sign([b.me_private]) block = b.create_block([tx]) b.write_block(block, durability='hard') @@ -1105,7 +1106,6 @@ class TestMultipleInputs(object): assert spent_inputs_user1 is None def test_get_spent_single_tx_multiple_outputs(self, b, user_sk, user_vk): - import random from bigchaindb.common import crypto from bigchaindb.models import Transaction from bigchaindb.common.transaction import Asset @@ -1158,7 +1158,8 @@ class TestMultipleInputs(object): transactions = [] for i in range(3): payload = {'somedata': random.randint(0, 255)} - tx = Transaction.create([b.me], [([user_vk, user2_vk], 1)], payload) + tx = Transaction.create([b.me], [([user_vk, user2_vk], 1)], + payload) tx = tx.sign([b.me_private]) transactions.append(tx) block = b.create_block(transactions) From efb5439044c1f036495483bcb142b8ad80c4ff14 Mon Sep 17 00:00:00 2001 From: Rodolphe Marques Date: Wed, 9 Nov 2016 14:56:14 +0100 Subject: [PATCH 16/54] updated docstrings addressed added todos --- bigchaindb/common/transaction.py | 31 +++++-------------------------- tests/common/test_transaction.py | 2 -- 2 files changed, 5 insertions(+), 28 deletions(-) diff --git a/bigchaindb/common/transaction.py b/bigchaindb/common/transaction.py index 8a8fcded..f058c001 100644 --- a/bigchaindb/common/transaction.py +++ b/bigchaindb/common/transaction.py @@ -274,7 +274,6 @@ class Condition(object): @classmethod def generate(cls, owners_after, amount): - # TODO: Update docstring """Generates a Condition from a specifically formed tuple or list. Note: @@ -284,25 +283,18 @@ class Condition(object): [(address|condition)*, [(address|condition)*, ...], ...] - If however, the thresholds of individual threshold conditions - to be created have to be set specifically, a tuple of the - following structure is necessary: - - ([(address|condition)*, - ([(address|condition)*, ...], subthreshold), - ...], threshold) - Args: - owners_after (:obj:`list` of :obj:`str`|tuple): The users that - should be able to fulfill the Condition that is being - created. + owners_after (:obj:`list` of :obj:`str`): The public key of + the users that should be able to fulfill the Condition + that is being created. + amount (:obj:`int`): The amount locked by the condition. Returns: A Condition that can be used in a Transaction. Returns: TypeError: If `owners_after` is not an instance of `list`. - TypeError: If `owners_after` is an empty list. + Value: If `owners_after` is an empty list. """ threshold = len(owners_after) if not isinstance(amount, int): @@ -686,7 +678,6 @@ class Transaction(object): @classmethod def create(cls, owners_before, owners_after, metadata=None, asset=None): - # TODO: Update docstring """A simple way to generate a `CREATE` transaction. Note: @@ -694,7 +685,6 @@ class Transaction(object): use cases: - Ed25519 - ThresholdSha256 - - PreimageSha256. Additionally, it provides support for the following BigchainDB use cases: @@ -709,10 +699,6 @@ class Transaction(object): Transaction. asset (:class:`~bigchaindb.common.transaction.Asset`): An Asset to be created in this Transaction. - secret (binarystr, optional): A secret string to create a hash- - lock Condition. - time_expire (int, optional): The UNIX time a Transaction is - valid. Returns: :class:`~bigchaindb.common.transaction.Transaction` @@ -1079,7 +1065,6 @@ class Transaction(object): """Splits multiple IO Transactions into partial single IO Transactions. """ - # TODO: Understand how conditions are being handled tx = Transaction(self.operation, self.asset, [fulfillment], self.conditions, self.metadata, self.timestamp, self.version) @@ -1092,12 +1077,6 @@ class Transaction(object): tx_serialized, input_condition_uri) - # TODO: Why?? Need to ask @TimDaub - # if not fulfillments_count == conditions_count == \ - # input_condition_uris_count: - # raise ValueError('Fulfillments, conditions and ' - # 'input_condition_uris must have the same count') - # else: if not fulfillments_count == input_condition_uris_count: raise ValueError('Fulfillments and ' 'input_condition_uris must have the same count') diff --git a/tests/common/test_transaction.py b/tests/common/test_transaction.py index 438c796c..e0094c61 100644 --- a/tests/common/test_transaction.py +++ b/tests/common/test_transaction.py @@ -598,7 +598,6 @@ def test_validate_multiple_fulfillments(user_ffill, user_cond, user_priv): from bigchaindb.common.crypto import SigningKey from bigchaindb.common.transaction import Transaction, Asset - # TODO: Why is there a fulfillment in the conditions list tx = Transaction(Transaction.CREATE, Asset(divisible=True), [user_ffill, deepcopy(user_ffill)], [user_cond, deepcopy(user_cond)]) @@ -792,7 +791,6 @@ def test_validate_multiple_io_create_transaction(user_pub, user_priv, user2_pub, user2_priv): from bigchaindb.common.transaction import Transaction, Asset - # TODO: Fix multiple owners_before in create transactions tx = Transaction.create([user_pub, user2_pub], [([user_pub], 1), ([user2_pub], 1)], metadata={'message': 'hello'}, From a2e28ae80623fae7683002c28629f24c7b7c2a78 Mon Sep 17 00:00:00 2001 From: Rodolphe Marques Date: Wed, 9 Nov 2016 17:48:39 +0100 Subject: [PATCH 17/54] addressed comments --- bigchaindb/common/transaction.py | 27 ++++---- bigchaindb/core.py | 6 +- bigchaindb/db/backends/rethinkdb.py | 6 +- bigchaindb/models.py | 4 +- tests/assets/test_digital_assets.py | 12 ++-- tests/assets/test_divisible_assets.py | 10 +-- tests/common/test_asset.py | 6 +- tests/common/test_transaction.py | 97 ++------------------------- 8 files changed, 44 insertions(+), 124 deletions(-) diff --git a/bigchaindb/common/transaction.py b/bigchaindb/common/transaction.py index f058c001..c53db530 100644 --- a/bigchaindb/common/transaction.py +++ b/bigchaindb/common/transaction.py @@ -413,7 +413,7 @@ class Asset(object): self.updatable = updatable self.refillable = refillable - self._validate_asset() + self.validate_asset() def __eq__(self, other): try: @@ -470,8 +470,9 @@ class Asset(object): transaction are related to the same asset id. Args: - transactions (list): list of transaction usually inputs that should - have a matching asset_id + transactions (:obj:`list` of :class:`~bigchaindb.common. + transaction.Transaction`): list of transaction usually inputs + that should have a matching asset_id Returns: str: uuid of the asset. @@ -488,11 +489,11 @@ class Asset(object): # 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.')) + raise AssetIdMismatch(('All inputs of all transactions passed' + ' need to have the same asset id')) return asset_ids.pop() - def _validate_asset(self, amount=None): + 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') @@ -670,11 +671,11 @@ class Transaction(object): if self.operation == self.CREATE: amount = sum([condition.amount for condition in self.conditions]) - self.asset._validate_asset(amount=amount) + self.asset.validate_asset(amount=amount) else: # In transactions other then `CREATE` we don't know if its a # divisible asset or not, so we cannot validate the amount here - self.asset._validate_asset() + self.asset.validate_asset() @classmethod def create(cls, owners_before, owners_after, metadata=None, asset=None): @@ -719,8 +720,9 @@ class Transaction(object): # generate_conditions for owner_after in owners_after: if not isinstance(owner_after, tuple) or len(owner_after) != 2: - raise ValueError(('Each `owner_after` in the list is a tuple' - ' of `([], )`')) + raise ValueError(('Each `owner_after` in the list must be a' + ' tuple of `([],' + ' )`')) pub_keys, amount = owner_after conds.append(Condition.generate(pub_keys, amount)) @@ -780,8 +782,9 @@ class Transaction(object): conds = [] for owner_after in owners_after: if not isinstance(owner_after, tuple) or len(owner_after) != 2: - raise ValueError(('Each `owner_after` in the list is a tuple' - ' of `([], )`')) + raise ValueError(('Each `owner_after` in the list must be a' + ' tuple of `([],' + ' )`')) pub_keys, amount = owner_after conds.append(Condition.generate(pub_keys, amount)) diff --git a/bigchaindb/core.py b/bigchaindb/core.py index 3c4f5347..5d58b644 100644 --- a/bigchaindb/core.py +++ b/bigchaindb/core.py @@ -345,14 +345,14 @@ 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 + """Returns the asset associated with an asset_id. Args: - asset_id (str): The asset id + asset_id (str): The asset id. Returns: :class:`~bigchaindb.common.transaction.Asset` if the asset - exists else None + exists else None. """ cursor = self.backend.get_asset_by_id(asset_id) cursor = list(cursor) diff --git a/bigchaindb/db/backends/rethinkdb.py b/bigchaindb/db/backends/rethinkdb.py index 944d5e7c..468e12a5 100644 --- a/bigchaindb/db/backends/rethinkdb.py +++ b/bigchaindb/db/backends/rethinkdb.py @@ -182,13 +182,13 @@ class RethinkDBBackend: transaction['transaction']['asset']['id'] == asset_id)) def get_asset_by_id(self, asset_id): - """Returns the asset associated with an asset_id + """Returns the asset associated with an asset_id. Args: - asset_id (str): The asset id + asset_id (str): The asset id. Returns: - Returns a rethinkdb cursor + Returns a rethinkdb cursor. """ return self.connection.run( r.table('bigchain', read_mode=self.read_mode) diff --git a/bigchaindb/models.py b/bigchaindb/models.py index 7f993ed4..c525e959 100644 --- a/bigchaindb/models.py +++ b/bigchaindb/models.py @@ -42,7 +42,7 @@ class Transaction(Transaction): raise ValueError('A CREATE operation has no inputs') # validate asset amount = sum([condition.amount for condition in self.conditions]) - self.asset._validate_asset(amount=amount) + self.asset.validate_asset(amount=amount) elif self.operation == Transaction.TRANSFER: if not inputs_defined: raise ValueError('Only `CREATE` transactions can have null ' @@ -85,7 +85,7 @@ class Transaction(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) + asset.validate_asset(amount=input_amount) # validate the amounts output_amount = sum([condition.amount for condition in self.conditions]) diff --git a/tests/assets/test_digital_assets.py b/tests/assets/test_digital_assets.py index 85d3a60a..56a08d53 100644 --- a/tests/assets/test_digital_assets.py +++ b/tests/assets/test_digital_assets.py @@ -25,7 +25,7 @@ def test_validate_bad_asset_creation(b, user_vk): # `divisible` needs to be a boolean tx = Transaction.create([b.me], [([user_vk], 1)]) tx.asset.divisible = 1 - with patch.object(Asset, '_validate_asset', return_value=None): + with patch.object(Asset, 'validate_asset', return_value=None): tx_signed = tx.sign([b.me_private]) with pytest.raises(TypeError): tx_signed.validate(b) @@ -33,7 +33,7 @@ def test_validate_bad_asset_creation(b, user_vk): # `refillable` needs to be a boolean tx = Transaction.create([b.me], [([user_vk], 1)]) tx.asset.refillable = 1 - with patch.object(Asset, '_validate_asset', return_value=None): + with patch.object(Asset, 'validate_asset', return_value=None): tx_signed = tx.sign([b.me_private]) with pytest.raises(TypeError): b.validate_transaction(tx_signed) @@ -41,7 +41,7 @@ def test_validate_bad_asset_creation(b, user_vk): # `updatable` needs to be a boolean tx = Transaction.create([b.me], [([user_vk], 1)]) tx.asset.updatable = 1 - with patch.object(Asset, '_validate_asset', return_value=None): + with patch.object(Asset, 'validate_asset', return_value=None): tx_signed = tx.sign([b.me_private]) with pytest.raises(TypeError): b.validate_transaction(tx_signed) @@ -49,7 +49,7 @@ def test_validate_bad_asset_creation(b, user_vk): # `data` needs to be a dictionary tx = Transaction.create([b.me], [([user_vk], 1)]) tx.asset.data = 'a' - with patch.object(Asset, '_validate_asset', return_value=None): + with patch.object(Asset, 'validate_asset', return_value=None): tx_signed = tx.sign([b.me_private]) with pytest.raises(TypeError): b.validate_transaction(tx_signed) @@ -189,7 +189,7 @@ def test_create_invalid_divisible_asset(b, user_vk, user_sk): # 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): + with patch.object(Asset, 'validate_asset', return_value=None): tx = Transaction.create([user_vk], [([user_vk], 2)], asset=asset) tx_signed = tx.sign([user_sk]) with pytest.raises(AmountError): @@ -197,7 +197,7 @@ def test_create_invalid_divisible_asset(b, user_vk, user_sk): assert b.is_valid_transaction(tx_signed) is False asset = Asset(divisible=True) - with patch.object(Asset, '_validate_asset', return_value=None): + with patch.object(Asset, 'validate_asset', return_value=None): tx = Transaction.create([user_vk], [([user_vk], 1)], asset=asset) tx_signed = tx.sign([user_sk]) with pytest.raises(AmountError): diff --git a/tests/assets/test_divisible_assets.py b/tests/assets/test_divisible_assets.py index d808008c..44181a4e 100644 --- a/tests/assets/test_divisible_assets.py +++ b/tests/assets/test_divisible_assets.py @@ -24,7 +24,7 @@ def test_single_in_single_own_single_out_single_own_create(b, user_vk): # CREATE divisible asset # Single input -# Single onwers_before +# Single owners_before # Multiple outputs # Single owners_after per output def test_single_in_single_own_multiple_out_single_own_create(b, user_vk): @@ -98,7 +98,7 @@ def test_single_in_single_own_multiple_out_mix_own_create(b, user_vk): # CREATE divisible asset # Single input # Multiple owners_before -# Ouput combinations already tested above +# Output combinations already tested above def test_single_in_multiple_own_single_out_single_own_create(b, user_vk, user_sk): from bigchaindb.models import Transaction @@ -519,7 +519,7 @@ def test_multiple_in_different_transactions(b, user_vk, user_sk): # `b` transfers its 50 shares to `user_vk` # after this transaction `user_vk` will have a total of 100 shares # split across two different transactions - tx_transfer1 = Transaction.transfer([tx_create.to_inputs()[1]], + tx_transfer1 = Transaction.transfer(tx_create.to_inputs([1]), [([user_vk], 50)], asset=tx_create.asset) tx_transfer1_signed = tx_transfer1.sign([b.me_private]) @@ -534,8 +534,8 @@ def test_multiple_in_different_transactions(b, user_vk, user_sk): # TRANSFER # `user_vk` combines two different transaction with 50 shares each and # transfers a total of 100 shares back to `b` - tx_transfer2 = Transaction.transfer([tx_create.to_inputs()[0], - tx_transfer1.to_inputs()[0]], + tx_transfer2 = Transaction.transfer(tx_create.to_inputs([0]) + + tx_transfer1.to_inputs([0]), [([b.me], 100)], asset=tx_create.asset) tx_transfer2_signed = tx_transfer2.sign([user_sk]) diff --git a/tests/common/test_asset.py b/tests/common/test_asset.py index cddaae64..f6a3f89d 100644 --- a/tests/common/test_asset.py +++ b/tests/common/test_asset.py @@ -81,12 +81,12 @@ def test_validate_asset(): # test amount errors asset = Asset(divisible=False) with raises(AmountError): - asset._validate_asset(amount=2) + asset.validate_asset(amount=2) asset = Asset(divisible=True) with raises(AmountError): - asset._validate_asset(amount=1) + asset.validate_asset(amount=1) asset = Asset() with raises(TypeError): - asset._validate_asset(amount='a') + asset.validate_asset(amount='a') diff --git a/tests/common/test_transaction.py b/tests/common/test_transaction.py index e0094c61..67304038 100644 --- a/tests/common/test_transaction.py +++ b/tests/common/test_transaction.py @@ -228,31 +228,6 @@ def test_generate_conditions_single_owner_with_condition(user_pub): assert cond.fulfillment.to_dict() == expected.to_dict() -# TODO FOR CC: see skip reason -@mark.skip(reason='threshold(hashlock).to_dict() exposes secret') -def test_generate_threshold_condition_with_hashlock(user_pub, user2_pub, - user3_pub): - from bigchaindb.common.transaction import Condition - from cryptoconditions import (PreimageSha256Fulfillment, - Ed25519Fulfillment, - ThresholdSha256Fulfillment) - - secret = b'much secret, wow' - hashlock = PreimageSha256Fulfillment(preimage=secret) - - expected_simple1 = Ed25519Fulfillment(public_key=user_pub) - expected_simple3 = Ed25519Fulfillment(public_key=user3_pub) - - expected = ThresholdSha256Fulfillment(threshold=2) - expected_sub = ThresholdSha256Fulfillment(threshold=2) - expected_sub.add_subfulfillment(expected_simple1) - expected_sub.add_subfulfillment(hashlock) - expected.add_subfulfillment(expected_simple3) - - cond = Condition.generate([[user_pub, hashlock], expected_simple3], 1) - assert cond.fulfillment.to_dict() == expected.to_dict() - - def test_generate_conditions_invalid_parameters(user_pub, user2_pub, user3_pub): from bigchaindb.common.transaction import Condition @@ -300,7 +275,7 @@ def test_invalid_transaction_initialization(): def test_create_default_asset_on_tx_initialization(): from bigchaindb.common.transaction import Transaction, Asset - with patch.object(Asset, '_validate_asset', return_value=None): + with patch.object(Asset, 'validate_asset', return_value=None): tx = Transaction(Transaction.CREATE, None) expected = Asset() asset = tx.asset @@ -493,7 +468,7 @@ def test_cast_transaction_link_to_boolean(): def test_add_fulfillment_to_tx(user_ffill): from bigchaindb.common.transaction import Transaction, Asset - with patch.object(Asset, '_validate_asset', return_value=None): + with patch.object(Asset, 'validate_asset', return_value=None): tx = Transaction(Transaction.CREATE, Asset(), [], []) tx.add_fulfillment(user_ffill) @@ -503,7 +478,7 @@ def test_add_fulfillment_to_tx(user_ffill): def test_add_fulfillment_to_tx_with_invalid_parameters(): from bigchaindb.common.transaction import Transaction, Asset - with patch.object(Asset, '_validate_asset', return_value=None): + with patch.object(Asset, 'validate_asset', return_value=None): tx = Transaction(Transaction.CREATE, Asset()) with raises(TypeError): tx.add_fulfillment('somewronginput') @@ -512,7 +487,7 @@ def test_add_fulfillment_to_tx_with_invalid_parameters(): def test_add_condition_to_tx(user_cond): from bigchaindb.common.transaction import Transaction, Asset - with patch.object(Asset, '_validate_asset', return_value=None): + with patch.object(Asset, 'validate_asset', return_value=None): tx = Transaction(Transaction.CREATE, Asset()) tx.add_condition(user_cond) @@ -522,7 +497,7 @@ def test_add_condition_to_tx(user_cond): def test_add_condition_to_tx_with_invalid_parameters(): from bigchaindb.common.transaction import Transaction, Asset - with patch.object(Asset, '_validate_asset', return_value=None): + with patch.object(Asset, 'validate_asset', return_value=None): tx = Transaction(Transaction.CREATE, Asset(), [], []) with raises(TypeError): tx.add_condition('somewronginput') @@ -605,9 +580,7 @@ def test_validate_multiple_fulfillments(user_ffill, user_cond, user_priv): expected_first = deepcopy(tx) expected_second = deepcopy(tx) expected_first.fulfillments = [expected_first.fulfillments[0]] - expected_first.conditions = expected_first.conditions expected_second.fulfillments = [expected_second.fulfillments[1]] - expected_second.conditions = expected_second.conditions expected_first_bytes = str(expected_first).encode() expected_first.fulfillments[0].fulfillment.sign(expected_first_bytes, @@ -854,62 +827,6 @@ def test_validate_threshold_create_transaction(user_pub, user_priv, user2_pub, assert tx.fulfillments_valid() is True -@mark.skip(reason='Hashlocks are not implemented') -def test_create_create_transaction_hashlock(user_pub, data, data_id): - from cryptoconditions import PreimageSha256Fulfillment - from bigchaindb.common.transaction import Transaction, Condition, Asset - - secret = b'much secret, wow' - hashlock = PreimageSha256Fulfillment(preimage=secret).condition_uri - cond = Condition(hashlock) - - expected = { - 'transaction': { - 'conditions': [cond.to_dict(0)], - 'metadata': { - 'data': data, - }, - 'asset': { - 'id': data_id, - 'divisible': False, - 'updatable': False, - 'refillable': False, - 'data': data, - }, - 'fulfillments': [ - { - 'owners_before': [ - user_pub, - ], - 'fid': 0, - 'fulfillment': None, - 'input': None - }, - ], - 'operation': 'CREATE', - }, - 'version': 1 - } - - asset = Asset(data, data_id) - tx = Transaction.create([user_pub], [], data, asset, secret).to_dict() - tx.pop('id') - tx['transaction']['metadata'].pop('id') - tx['transaction'].pop('timestamp') - tx['transaction']['fulfillments'][0]['fulfillment'] = None - - assert tx == expected - - -@mark.skip(reson='Hashlocks are not implemented') -def test_validate_hashlock_create_transaction(user_pub, user_priv, data): - from bigchaindb.common.transaction import Transaction, Asset - - tx = Transaction.create([user_pub], [], data, Asset(), b'much secret, wow') - tx = tx.sign([user_priv]) - assert tx.fulfillments_valid() is True - - def test_create_create_transaction_with_invalid_parameters(user_pub): from bigchaindb.common.transaction import Transaction @@ -1071,7 +988,7 @@ def test_create_transfer_with_invalid_parameters(user_pub): def test_cant_add_empty_condition(): from bigchaindb.common.transaction import Transaction, Asset - with patch.object(Asset, '_validate_asset', return_value=None): + with patch.object(Asset, 'validate_asset', return_value=None): tx = Transaction(Transaction.CREATE, None) with raises(TypeError): tx.add_condition(None) @@ -1080,7 +997,7 @@ def test_cant_add_empty_condition(): def test_cant_add_empty_fulfillment(): from bigchaindb.common.transaction import Transaction, Asset - with patch.object(Asset, '_validate_asset', return_value=None): + with patch.object(Asset, 'validate_asset', return_value=None): tx = Transaction(Transaction.CREATE, None) with raises(TypeError): tx.add_fulfillment(None) From ed09cf3c8e4124a890fac7c43e9824380827eead Mon Sep 17 00:00:00 2001 From: Troy McConaghy Date: Wed, 9 Nov 2016 18:40:16 +0100 Subject: [PATCH 18/54] Created a file to outline our release process --- Release_Process.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 Release_Process.md diff --git a/Release_Process.md b/Release_Process.md new file mode 100644 index 00000000..dbfd10ed --- /dev/null +++ b/Release_Process.md @@ -0,0 +1,23 @@ +# Our Release Process + +This is a summary of the steps we go through to release a new version of BigchainDB Server. + +1. Update the `CHANGELOG.md` file +2. Update `bigchaindb/version.py` +3. Go to the [bigchaindb/bigchaindb Releases page on GitHub](https://github.com/bigchaindb/bigchaindb/releases) + and click the "Draft a new release" button +4. Name the tag something like v0.7.0 +5. The target should be a specific commit: the one when the update of `bigchaindb/version.py` got merged into master +6. The release title should be something like v0.7.0 +7. The description should be copied from the `CHANGELOG.md` file updated above +8. Generate and send the latest `bigchaindb` package to PyPI. Dimi and Sylvain can do this, maybe others +9. Login to readthedocs.org as a maintainer of the BigchainDB Server docs. + Go to Admin --> Versions and under **Choose Active Versions**, make sure that the new version's tag is + "Active" and "Public" + +After the release: + +1. Update `bigchaindb/version.py` again, to be something like 0.8.0.dev (with a dev on the end). +This is so people reading the latest docs will know that they're for the latest (master branch) +version of BigchainDB Server, not the docs at the time of the most recent release (which are also +available). From 2fef20148904e600ba74926b627235f2ecc85f7d Mon Sep 17 00:00:00 2001 From: Troy McConaghy Date: Thu, 10 Nov 2016 10:22:29 +0100 Subject: [PATCH 19/54] Fixed broken link in CONTRIBUTING.md file Fixed the link to the setuptools documentation about "extras", i.e. the `extras_require` bit of `setup.py`. --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 92137572..cf3a552c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -82,7 +82,7 @@ How? Let's split the command down into its components: - `install` tells pip to use the *install* action - `-e` installs a project in [editable mode](https://pip.pypa.io/en/stable/reference/pip_install/#editable-installs) - `.` installs what's in the current directory - - `[dev]` adds some [extra requirements](https://pythonhosted.org/setuptools/setuptools.html#declaring-extras-optional-features-with-their-own-dependencies) to the installation. (If you are curious, open `setup.py` and look for `dev` in the `extras_require` section.) + - `[dev]` adds some [extra requirements](https://setuptools.readthedocs.io/en/latest/setuptools.html#declaring-extras-optional-features-with-their-own-dependencies) to the installation. (If you are curious, open `setup.py` and look for `dev` in the `extras_require` section.) Aside: An alternative to `pip install -e .[dev]` is `python setup.py develop`. From d8be9ac3ec81ce960d85143fed78d886175840ed Mon Sep 17 00:00:00 2001 From: Rodolphe Marques Date: Thu, 10 Nov 2016 11:59:22 +0100 Subject: [PATCH 20/54] Fixed typo Renamed some variables to make the code more readable --- bigchaindb/common/transaction.py | 16 ++++++++-------- bigchaindb/models.py | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/bigchaindb/common/transaction.py b/bigchaindb/common/transaction.py index c53db530..16ba3585 100644 --- a/bigchaindb/common/transaction.py +++ b/bigchaindb/common/transaction.py @@ -714,8 +714,8 @@ class Transaction(object): raise ValueError('`owners_after` list cannot be empty') metadata = Metadata(metadata) - ffills = [] - conds = [] + fulfillments = [] + conditions = [] # generate_conditions for owner_after in owners_after: @@ -724,12 +724,12 @@ class Transaction(object): ' tuple of `([],' ' )`')) pub_keys, amount = owner_after - conds.append(Condition.generate(pub_keys, amount)) + conditions.append(Condition.generate(pub_keys, amount)) # generate fulfillments - ffills.append(Fulfillment.generate(owners_before)) + fulfillments.append(Fulfillment.generate(owners_before)) - return cls(cls.CREATE, asset, ffills, conds, metadata) + return cls(cls.CREATE, asset, fulfillments, conditions, metadata) @classmethod def transfer(cls, inputs, owners_after, asset, metadata=None): @@ -779,18 +779,18 @@ class Transaction(object): if len(owners_after) == 0: raise ValueError('`owners_after` list cannot be empty') - conds = [] + conditions = [] for owner_after in owners_after: if not isinstance(owner_after, tuple) or len(owner_after) != 2: raise ValueError(('Each `owner_after` in the list must be a' ' tuple of `([],' ' )`')) pub_keys, amount = owner_after - conds.append(Condition.generate(pub_keys, amount)) + conditions.append(Condition.generate(pub_keys, amount)) metadata = Metadata(metadata) inputs = deepcopy(inputs) - return cls(cls.TRANSFER, asset, inputs, conds, metadata) + return cls(cls.TRANSFER, asset, inputs, conditions, metadata) def __eq__(self, other): try: diff --git a/bigchaindb/models.py b/bigchaindb/models.py index c525e959..dc78ccfa 100644 --- a/bigchaindb/models.py +++ b/bigchaindb/models.py @@ -90,7 +90,7 @@ class Transaction(Transaction): output_amount = sum([condition.amount for condition in self.conditions]) if output_amount != input_amount: - raise AmountError(('The amout used in the inputs `{}`' + raise AmountError(('The amount used in the inputs `{}`' ' needs to be same as the amount used' ' in the outputs `{}`') .format(input_amount, output_amount)) From 70aec6eb4666dd01439d5c0beccf7f9b43f44bda Mon Sep 17 00:00:00 2001 From: Rodolphe Marques Date: Thu, 10 Nov 2016 12:03:01 +0100 Subject: [PATCH 21/54] cleanup code --- bigchaindb/common/transaction.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bigchaindb/common/transaction.py b/bigchaindb/common/transaction.py index 16ba3585..c1a67ed3 100644 --- a/bigchaindb/common/transaction.py +++ b/bigchaindb/common/transaction.py @@ -1083,9 +1083,9 @@ class Transaction(object): if not fulfillments_count == input_condition_uris_count: raise ValueError('Fulfillments and ' 'input_condition_uris must have the same count') - else: - partial_transactions = map(gen_tx, self.fulfillments, - self.conditions, input_condition_uris) + + partial_transactions = map(gen_tx, self.fulfillments, + self.conditions, input_condition_uris) return all(partial_transactions) @staticmethod From bca7939f6cc7091e7a35ed6ada4b9a6241ccf1c8 Mon Sep 17 00:00:00 2001 From: Rodolphe Marques Date: Thu, 10 Nov 2016 13:18:20 +0100 Subject: [PATCH 22/54] Added an AssetLink class to link to a Asset from a TRANSFER transaction --- bigchaindb/common/transaction.py | 60 ++++++++++++++++++++++++++++---- tests/common/test_transaction.py | 53 +++++++++++++++++++++++++++- 2 files changed, 106 insertions(+), 7 deletions(-) diff --git a/bigchaindb/common/transaction.py b/bigchaindb/common/transaction.py index c1a67ed3..71f56d82 100644 --- a/bigchaindb/common/transaction.py +++ b/bigchaindb/common/transaction.py @@ -528,6 +528,54 @@ class Asset(object): ' greater than one') +class AssetLink(Asset): + """An object for unidirectional linking to a Asset. + """ + + def __init__(self, data_id=None): + """Used to point to a specific Asset. + + Args: + data_id (str): A Asset to link to. + """ + self.data_id = data_id + + def __bool__(self): + return self.data_id is not None + + def __eq__(self, other): + return isinstance(other, AssetLink) and \ + self.to_dict() == self.to_dict() + + @classmethod + def from_dict(cls, link): + """Transforms a Python dictionary to a AssetLink object. + + Args: + link (dict): The link to be transformed. + + Returns: + :class:`~bigchaindb.common.transaction.AssetLink` + """ + try: + return cls(link['id']) + except TypeError: + return cls() + + def to_dict(self): + """Transforms the object to a Python dictionary. + + Returns: + (dict|None): The link as an alternative serialization format. + """ + if self.data_id is None: + return None + else: + return { + 'id': self.data_id + } + + class Metadata(object): """Metadata is used to store a dictionary and its hash in a Transaction.""" @@ -668,14 +716,11 @@ class Transaction(object): # validate asset # we know that each transaction relates to a single asset # we can sum the amount of all the conditions - + # for transactions other then CREATE we only have an id so there is + # nothing we can validate if self.operation == self.CREATE: amount = sum([condition.amount for condition in self.conditions]) self.asset.validate_asset(amount=amount) - else: - # In transactions other then `CREATE` we don't know if its a - # divisible asset or not, so we cannot validate the amount here - self.asset.validate_asset() @classmethod def create(cls, owners_before, owners_after, metadata=None, asset=None): @@ -1244,7 +1289,10 @@ class Transaction(object): conditions = [Condition.from_dict(condition) for condition in tx['conditions']] metadata = Metadata.from_dict(tx['metadata']) - asset = Asset.from_dict(tx['asset']) + if tx['operation'] in [cls.CREATE, cls.GENESIS]: + asset = Asset.from_dict(tx['asset']) + else: + asset = AssetLink.from_dict(tx['asset']) return cls(tx['operation'], asset, fulfillments, conditions, metadata, tx['timestamp'], tx_body['version']) diff --git a/tests/common/test_transaction.py b/tests/common/test_transaction.py index 67304038..baba35af 100644 --- a/tests/common/test_transaction.py +++ b/tests/common/test_transaction.py @@ -1,4 +1,4 @@ -from pytest import raises, mark +from pytest import raises from unittest.mock import patch @@ -465,6 +465,57 @@ def test_cast_transaction_link_to_boolean(): assert bool(TransactionLink(False, False)) is True +def test_asset_link_serialization(): + from bigchaindb.common.transaction import AssetLink + + data_id = 'a asset id' + expected = { + 'id': data_id, + } + asset_link = AssetLink(data_id) + + assert asset_link.to_dict() == expected + + +def test_asset_link_serialization_with_empty_payload(): + from bigchaindb.common.transaction import AssetLink + + expected = None + asset_link = AssetLink() + + assert asset_link.to_dict() == expected + + +def test_asset_link_deserialization(): + from bigchaindb.common.transaction import AssetLink + + data_id = 'a asset id' + expected = AssetLink(data_id) + asset_link = { + 'id': data_id + } + asset_link = AssetLink.from_dict(asset_link) + + assert asset_link == expected + + +def test_asset_link_deserialization_with_empty_payload(): + from bigchaindb.common.transaction import AssetLink + + expected = AssetLink() + asset_link = AssetLink.from_dict(None) + + assert asset_link == expected + + +def test_cast_asset_link_to_boolean(): + from bigchaindb.common.transaction import AssetLink + + assert bool(AssetLink()) is False + assert bool(AssetLink('a')) is True + assert bool(AssetLink(False)) is True + + def test_add_fulfillment_to_tx(user_ffill): from bigchaindb.common.transaction import Transaction, Asset From 6d9cdd0bb5c9390457818b3d45a80c79e126ed66 Mon Sep 17 00:00:00 2001 From: Rodolphe Marques Date: Thu, 10 Nov 2016 13:20:09 +0100 Subject: [PATCH 23/54] removed addressed todo --- bigchaindb/common/transaction.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/bigchaindb/common/transaction.py b/bigchaindb/common/transaction.py index 71f56d82..213eb3d3 100644 --- a/bigchaindb/common/transaction.py +++ b/bigchaindb/common/transaction.py @@ -447,12 +447,6 @@ class Asset(object): Returns: :class:`~bigchaindb.common.transaction.Asset` """ - # TODO: This is not correct. If using Transaction.from_dict() from a - # TRANSFER transaction we only have information about the `id`, - # meaning that even if its a divisible asset, since the key does - # not exist if will be set to False by default. - # Maybe use something like an AssetLink similar to - # TransactionLink for TRANSFER transactions return cls(asset.get('data'), asset['id'], asset.get('divisible', False), asset.get('updatable', False), From c30c32bcd57b987aa9e9c8f240650a89a06c78c6 Mon Sep 17 00:00:00 2001 From: troymc Date: Thu, 10 Nov 2016 16:44:42 +0100 Subject: [PATCH 24/54] In Release_Process.md, noted we use semantic versioning --- Release_Process.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Release_Process.md b/Release_Process.md index dbfd10ed..bb826ae9 100644 --- a/Release_Process.md +++ b/Release_Process.md @@ -3,7 +3,7 @@ This is a summary of the steps we go through to release a new version of BigchainDB Server. 1. Update the `CHANGELOG.md` file -2. Update `bigchaindb/version.py` +2. Update the version numbers in `bigchaindb/version.py`. Note that we try to use [semantic versioning](http://semver.org/) (i.e. MAJOR.MINOR.PATCH) 3. Go to the [bigchaindb/bigchaindb Releases page on GitHub](https://github.com/bigchaindb/bigchaindb/releases) and click the "Draft a new release" button 4. Name the tag something like v0.7.0 From 6d7392d98df7f0800a0c8092f2ccbf95b1697bc1 Mon Sep 17 00:00:00 2001 From: Rodolphe Marques Date: Fri, 11 Nov 2016 11:34:20 +0100 Subject: [PATCH 25/54] Handle the case where there are negative amounts. Created tests --- bigchaindb/common/transaction.py | 2 + bigchaindb/models.py | 10 ++- tests/assets/test_divisible_assets.py | 93 +++++++++++++++++++++++++++ 3 files changed, 103 insertions(+), 2 deletions(-) diff --git a/bigchaindb/common/transaction.py b/bigchaindb/common/transaction.py index 213eb3d3..849153af 100644 --- a/bigchaindb/common/transaction.py +++ b/bigchaindb/common/transaction.py @@ -299,6 +299,8 @@ class Condition(object): threshold = len(owners_after) if not isinstance(amount, int): raise TypeError('`amount` must be a int') + if amount < 1: + raise AmountError('`amount` needs to be greater than zero') if not isinstance(owners_after, list): raise TypeError('`owners_after` must be an instance of list') if len(owners_after) == 0: diff --git a/bigchaindb/models.py b/bigchaindb/models.py index dc78ccfa..2d888af9 100644 --- a/bigchaindb/models.py +++ b/bigchaindb/models.py @@ -73,6 +73,8 @@ class Transaction(Transaction): input_conditions.append(input_tx.conditions[input_cid]) input_txs.append(input_tx) + if input_tx.conditions[input_cid].amount < 1: + raise AmountError('`amount` needs to be greater than zero') input_amount += input_tx.conditions[input_cid].amount # validate asset id @@ -87,8 +89,12 @@ class Transaction(Transaction): # validate the asset asset.validate_asset(amount=input_amount) # validate the amounts - output_amount = sum([condition.amount for - condition in self.conditions]) + output_amount = 0 + for condition in self.conditions: + if condition.amount < 1: + raise AmountError('`amount` needs to be greater than zero') + output_amount += condition.amount + if output_amount != input_amount: raise AmountError(('The amount used in the inputs `{}`' ' needs to be same as the amount used' diff --git a/tests/assets/test_divisible_assets.py b/tests/assets/test_divisible_assets.py index 44181a4e..5ae360e0 100644 --- a/tests/assets/test_divisible_assets.py +++ b/tests/assets/test_divisible_assets.py @@ -1,5 +1,7 @@ import pytest +from unittest.mock import patch + from ..db.conftest import inputs # noqa @@ -684,3 +686,94 @@ def test_divide(b, user_vk, user_sk): assert len(tx_transfer_signed.conditions) == 3 for condition in tx_transfer_signed.conditions: assert condition.amount == 1 + + +# Check that negative inputs are caught when creating a TRANSFER transaction +@pytest.mark.usefixtures('inputs') +def test_non_positive_amounts_on_transfer(b, user_vk): + from bigchaindb.models import Transaction + from bigchaindb.common.transaction import Asset + from bigchaindb.common.exceptions import AmountError + + # CREATE divisible asset with 1 output with amount 3 + asset = Asset(divisible=True) + tx_create = Transaction.create([b.me], [([user_vk], 3)], + 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) + + with pytest.raises(AmountError): + Transaction.transfer(tx_create.to_inputs(), + [([b.me], 4), ([b.me], -1)], + asset=tx_create.asset) + + +# Check that negative inputs are caught when validating a TRANSFER transaction +@pytest.mark.usefixtures('inputs') +def test_non_positive_amounts_on_transfer_validate(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 with 1 output with amount 3 + asset = Asset(divisible=True) + tx_create = Transaction.create([b.me], [([user_vk], 3)], + 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) + + # create a transfer transaction with 3 outputs and check if the amount + # of each output is 1 + tx_transfer = Transaction.transfer(tx_create.to_inputs(), + [([b.me], 4), ([b.me], 1)], + asset=tx_create.asset) + tx_transfer.conditions[1].amount = -1 + tx_transfer_signed = tx_transfer.sign([user_sk]) + + with pytest.raises(AmountError): + tx_transfer_signed.validate(b) + + +# Check that negative inputs are caught when creating a CREATE transaction +@pytest.mark.usefixtures('inputs') +def test_non_positive_amounts_on_create(b, user_vk): + from bigchaindb.models import Transaction + from bigchaindb.common.transaction import Asset + from bigchaindb.common.exceptions import AmountError + + # CREATE divisible asset with 1 output with amount 3 + asset = Asset(divisible=True) + with pytest.raises(AmountError): + Transaction.create([b.me], [([user_vk], -3)], + asset=asset) + + +# Check that negative inputs are caught when validating a CREATE transaction +@pytest.mark.usefixtures('inputs') +def test_non_positive_amounts_on_create_validate(b, user_vk): + from bigchaindb.models import Transaction + from bigchaindb.common.transaction import Asset + from bigchaindb.common.exceptions import AmountError + + # CREATE divisible asset with 1 output with amount 3 + asset = Asset(divisible=True) + tx_create = Transaction.create([b.me], [([user_vk], 3)], + asset=asset) + tx_create.conditions[0].amount = -3 + with patch.object(Asset, 'validate_asset', return_value=None): + tx_create_signed = tx_create.sign([b.me_private]) + + with pytest.raises(AmountError): + tx_create_signed.validate(b) From d31a268a51671ff45736df08ff76bdbf055d025c Mon Sep 17 00:00:00 2001 From: Ryan Henderson Date: Fri, 11 Nov 2016 15:08:37 +0100 Subject: [PATCH 26/54] add id query (#799) --- bigchaindb/core.py | 22 ++++++++++++++++++++++ bigchaindb/db/backends/rethinkdb.py | 11 +++++++++++ tests/db/test_bigchain_api.py | 9 +++++++++ 3 files changed, 42 insertions(+) diff --git a/bigchaindb/core.py b/bigchaindb/core.py index 5a007eab..7711514a 100644 --- a/bigchaindb/core.py +++ b/bigchaindb/core.py @@ -188,6 +188,28 @@ class Bigchain(object): exceptions.FulfillmentNotInValidBlock): return False + def get_block(self, block_id, include_status=False): + """Get the block with the specified `block_id` (and optionally its status) + + Returns the block corresponding to `block_id` or None if no match is + found. + + Args: + block_id (str): transaction id of the transaction to get + include_status (bool): also return the status of the block + the return value is then a tuple: (block, status) + """ + block = self.backend.get_block(block_id) + status = None + + if include_status: + if block: + status = self.block_election_status(block_id, + block['block']['voters']) + return block, status + else: + return block + def get_transaction(self, txid, include_status=False): """Get the transaction with the specified `txid` (and optionally its status) diff --git a/bigchaindb/db/backends/rethinkdb.py b/bigchaindb/db/backends/rethinkdb.py index 22937dd2..5b73cce6 100644 --- a/bigchaindb/db/backends/rethinkdb.py +++ b/bigchaindb/db/backends/rethinkdb.py @@ -258,6 +258,17 @@ class RethinkDBBackend: r.table('bigchain') .insert(r.json(block), durability=durability)) + def get_block(self, block_id): + """Get a block from the bigchain table + + Args: + block_id (str): block id of the block to get + + Returns: + block (dict): the block or `None` + """ + return self.connection.run(r.table('bigchain').get(block_id)) + def has_transaction(self, transaction_id): """Check if a transaction exists in the bigchain table. diff --git a/tests/db/test_bigchain_api.py b/tests/db/test_bigchain_api.py index a6b76eb4..314286c6 100644 --- a/tests/db/test_bigchain_api.py +++ b/tests/db/test_bigchain_api.py @@ -369,6 +369,15 @@ class TestBigchainApi(object): assert excinfo.value.args[0] == 'Empty block creation is not allowed' + @pytest.mark.usefixtures('inputs') + def test_get_block_by_id(self, b): + new_block = dummy_block() + b.write_block(new_block, durability='hard') + + assert b.get_block(new_block.id) == new_block.to_dict() + block, status = b.get_block(new_block.id, include_status=True) + assert status == b.BLOCK_UNDECIDED + def test_get_last_voted_block_returns_genesis_if_no_votes_has_been_casted(self, b): import rethinkdb as r from bigchaindb import util From 1c8fec730c15be3c3527d9ae8609c19e8fae5a8a Mon Sep 17 00:00:00 2001 From: tim Date: Thu, 10 Nov 2016 11:02:23 +0100 Subject: [PATCH 27/54] Format http api docs to 79 chars --- .../http-client-server-api.rst | 44 ++++++++++++++----- 1 file changed, 33 insertions(+), 11 deletions(-) diff --git a/docs/server/source/drivers-clients/http-client-server-api.rst b/docs/server/source/drivers-clients/http-client-server-api.rst index 74bcf1d2..d274efad 100644 --- a/docs/server/source/drivers-clients/http-client-server-api.rst +++ b/docs/server/source/drivers-clients/http-client-server-api.rst @@ -3,21 +3,35 @@ The HTTP Client-Server API .. note:: - The HTTP client-server API is currently quite rudimentary. For example, there is no ability to do complex queries using the HTTP API. We plan to add querying capabilities in the future. + The HTTP client-server API is currently quite rudimentary. For example, + there is no ability to do complex queries using the HTTP API. We plan to add + querying capabilities in the future. -When you start Bigchaindb using `bigchaindb start`, an HTTP API is exposed at the address stored in the BigchainDB node configuration settings. The default is: +When you start Bigchaindb using `bigchaindb start`, an HTTP API is exposed at +the address stored in the BigchainDB node configuration settings. The default +is: `http://localhost:9984/api/v1/ `_ -but that address can be changed by changing the "API endpoint" configuration setting (e.g. in a local config file). There's more information about setting the API endpoint in :doc:`the section about BigchainDB Configuration Settings <../server-reference/configuration>`. +but that address can be changed by changing the "API endpoint" configuration +setting (e.g. in a local config file). There's more information about setting +the API endpoint in :doc:`the section about BigchainDB Configuration Settings +<../server-reference/configuration>`. -There are other configuration settings related to the web server (serving the HTTP API). In particular, the default is for the web server socket to bind to ``localhost:9984`` but that can be changed (e.g. to ``0.0.0.0:9984``). For more details, see the "server" settings ("bind", "workers" and "threads") in :doc:`the section about BigchainDB Configuration Settings <../server-reference/configuration>`. +There are other configuration settings related to the web server (serving the +HTTP API). In particular, the default is for the web server socket to bind to +``localhost:9984`` but that can be changed (e.g. to ``0.0.0.0:9984``). For more +details, see the "server" settings ("bind", "workers" and "threads") in +:doc:`the section about BigchainDB Configuration Settings +<../server-reference/configuration>`. API Root -------- -If you send an HTTP GET request to e.g. ``http://localhost:9984`` (with no ``/api/v1/`` on the end), then you should get an HTTP response with something like the following in the body: +If you send an HTTP GET request to e.g. ``http://localhost:9984`` (with no +``/api/v1/`` on the end), then you should get an HTTP response with something +like the following in the body: .. code-block:: json @@ -39,8 +53,13 @@ POST /transactions/ .. http:post:: /transactions/ Push a new transaction. - - Note: The posted transaction should be valid `transaction `_. The steps to build a valid transaction are beyond the scope of this page. One would normally use a driver such as the `BigchainDB Python Driver `_ to build a valid transaction. + + Note: The posted transaction should be valid `transaction + `_. + The steps to build a valid transaction are beyond the scope of this page. + One would normally use a driver such as the `BigchainDB Python Driver + `_ to + build a valid transaction. **Example request**: @@ -158,9 +177,11 @@ GET /transactions/{tx_id}/status .. http:get:: /transactions/{tx_id}/status - Get the status of the transaction with the ID ``tx_id``, if a transaction with that ``tx_id`` exists. + Get the status of the transaction with the ID ``tx_id``, if a transaction + with that ``tx_id`` exists. - The possible status values are ``backlog``, ``undecided``, ``valid`` or ``invalid``. + The possible status values are ``backlog``, ``undecided``, ``valid`` or + ``invalid``. :param tx_id: transaction ID :type tx_id: hex string @@ -194,7 +215,8 @@ GET /transactions/{tx_id} Get the transaction with the ID ``tx_id``. - This endpoint returns only a transaction from a ``VALID`` or ``UNDECIDED`` block on ``bigchain``, if exists. + This endpoint returns only a transaction from a ``VALID`` or ``UNDECIDED`` + block on ``bigchain``, if exists. :param tx_id: transaction ID :type tx_id: hex string @@ -260,4 +282,4 @@ GET /transactions/{tx_id} } :statuscode 200: A transaction with that ID was found. - :statuscode 404: A transaction with that ID was not found. \ No newline at end of file + :statuscode 404: A transaction with that ID was not found. From c8553abb4146e2b290109753608d71bd5074f0ee Mon Sep 17 00:00:00 2001 From: Ryan Henderson Date: Mon, 14 Nov 2016 10:03:59 +0100 Subject: [PATCH 28/54] add backlog count (#806) --- bigchaindb/db/backends/rethinkdb.py | 11 +++++++++++ tests/db/test_bigchain_api.py | 9 +++++++++ 2 files changed, 20 insertions(+) diff --git a/bigchaindb/db/backends/rethinkdb.py b/bigchaindb/db/backends/rethinkdb.py index 5b73cce6..aeb68572 100644 --- a/bigchaindb/db/backends/rethinkdb.py +++ b/bigchaindb/db/backends/rethinkdb.py @@ -293,6 +293,17 @@ class RethinkDBBackend: r.table('bigchain', read_mode=self.read_mode) .count()) + def count_backlog(self): + """Count the number of transactions in the backlog table. + + Returns: + The number of transactions in the backlog. + """ + + return self.connection.run( + r.table('backlog', read_mode=self.read_mode) + .count()) + def write_vote(self, vote): """Write a vote to the votes table. diff --git a/tests/db/test_bigchain_api.py b/tests/db/test_bigchain_api.py index 314286c6..6a7880e9 100644 --- a/tests/db/test_bigchain_api.py +++ b/tests/db/test_bigchain_api.py @@ -592,6 +592,15 @@ class TestBigchainApi(object): with pytest.raises(TransactionDoesNotExist) as excinfo: tx.validate(Bigchain()) + def test_count_backlog(self, b, user_vk): + from bigchaindb.models import Transaction + + for _ in range(4): + tx = Transaction.create([b.me], [user_vk]).sign([b.me_private]) + b.write_transaction(tx) + + assert b.backend.count_backlog() == 4 + class TestTransactionValidation(object): def test_create_operation_with_inputs(self, b, user_vk, create_tx): From 29832f9a9bd72cbff855b2150c4a3568b00eda21 Mon Sep 17 00:00:00 2001 From: Ryan Henderson Date: Mon, 14 Nov 2016 10:04:28 +0100 Subject: [PATCH 29/54] add get_genesis_block (#809) --- bigchaindb/db/backends/rethinkdb.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/bigchaindb/db/backends/rethinkdb.py b/bigchaindb/db/backends/rethinkdb.py index aeb68572..c9a171e9 100644 --- a/bigchaindb/db/backends/rethinkdb.py +++ b/bigchaindb/db/backends/rethinkdb.py @@ -317,6 +317,17 @@ class RethinkDBBackend: r.table('votes') .insert(vote)) + def get_genesis_block(self): + """Get the genesis block + + Returns: + The genesis block + """ + return self.connection.run( + r.table('bigchain', read_mode=self.read_mode) + .filter(util.is_genesis_block) + .nth(0)) + def get_last_voted_block(self, node_pubkey): """Get the last voted block for a specific node. @@ -341,10 +352,7 @@ class RethinkDBBackend: except r.ReqlNonExistenceError: # return last vote if last vote exists else return Genesis block - return self.connection.run( - r.table('bigchain', read_mode=self.read_mode) - .filter(util.is_genesis_block) - .nth(0)) + return self.get_genesis_block() # Now the fun starts. Since the resolution of timestamp is a second, # we might have more than one vote per timestamp. If this is the case From f69ccc93df916d273bea61ca30128c21d68ce1a7 Mon Sep 17 00:00:00 2001 From: tim Date: Mon, 17 Oct 2016 15:00:38 +0200 Subject: [PATCH 30/54] Add doc strings for Block cls --- bigchaindb/models.py | 86 +++++++++++++++++++++++++++++++++++++++----- 1 file changed, 78 insertions(+), 8 deletions(-) diff --git a/bigchaindb/models.py b/bigchaindb/models.py index 6471b075..6d2ff053 100644 --- a/bigchaindb/models.py +++ b/bigchaindb/models.py @@ -118,8 +118,36 @@ class Transaction(Transaction): class Block(object): + """A Block bundles up to 1000 Transactions. Nodes vote on its validity. + + 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. + voters (:obj:`list` of :obj:`str`): A list of a federation + nodes' public keys supposed to vote on the Block. + signature (str): A cryptographic signature ensuring the + integrity and creatorship of a Block. + """ + def __init__(self, transactions=None, node_pubkey=None, timestamp=None, voters=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. + voters (:obj:`list` of :obj:`str`): A list of a federation + nodes' public keys supposed to vote on the Block. + signature (str): A cryptographic signature ensuring the + integrity and creatorship of a Block. + """ if transactions is not None and not isinstance(transactions, list): raise TypeError('`transactions` must be a list instance or None') else: @@ -146,18 +174,20 @@ class Block(object): return self.to_dict() == other def validate(self, bigchain): - """Validate a block. + """Validates the Block. Args: - bigchain (Bigchain): an instantiated bigchaindb.Bigchain object. + bigchain (:class:`~bigchaindb.Bigchain`): An instantiated Bigchain + object. Returns: - block (Block): The block as a `Block` object if it is valid. - Else it raises an appropriate exception describing - the reason of invalidity. + :class:`~.Block`: If valid, returns a `Block` object. Else an + appropriate exception describing the reason of invalidity is + raised. Raises: - OperationError: if a non-federation node signed the block. + OperationError: If a non-federation node signed the Block. + InvalidSignature: If a Block's signature is invalid. """ # First, make sure this node hasn't already voted on this block @@ -182,6 +212,15 @@ class Block(object): return self def sign(self, signing_key): + """Creates a signature for the Block and overwrites `self.signature`. + + Args: + signing_key (str): A signing key corresponding to + `self.node_pubkey`. + + Returns: + :class:`~.Block` + """ block_body = self.to_dict() block_serialized = serialize(block_body['block']) signing_key = SigningKey(signing_key) @@ -189,8 +228,13 @@ class Block(object): return self def is_signature_valid(self): + """Checks 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 bytesting messages + # cc only accepts bytestring messages block_serialized = serialize(block).encode() verifying_key = VerifyingKey(block['node_pubkey']) try: @@ -202,6 +246,24 @@ class Block(object): @classmethod def from_dict(cls, block_body): + """Transforms a Python dictionary to a Block object. + + Note: + Throws errors if passed signature or id is incorrect. + + Args: + block_body (dict): A block dictionary to be transformed. + + Returns: + :class:`~Block` + + Raises: + InvalidHash: If the block's id is not corresponding to its + data. + InvalidSignature: If the block's signature is not corresponding + to it's data or `node_pubkey`. + """ + # TODO: Reuse `is_signature_valid` method here. block = block_body['block'] block_serialized = serialize(block) block_id = hash_data(block_serialized) @@ -220,7 +282,7 @@ class Block(object): # https://github.com/bigchaindb/cryptoconditions/issues/27 try: signature_valid = verifying_key\ - .verify(block_serialized.encode(), signature) + .verify(block_serialized.encode(), signature) except ValueError: signature_valid = False if signature_valid is False: @@ -237,6 +299,14 @@ class Block(object): return self.to_dict()['id'] def to_dict(self): + """Transforms the object to a Python dictionary. + + Returns: + dict: The Block as an alternative serialization format. + + Raises: + OperationError: If a Block doesn't contain any transactions. + """ if len(self.transactions) == 0: raise OperationError('Empty block creation is not allowed') From b42eec93c7b05df09191e0afac9676c65f7f6fcc Mon Sep 17 00:00:00 2001 From: tim Date: Mon, 14 Nov 2016 13:53:49 +0100 Subject: [PATCH 31/54] Fix indentation for model docs --- bigchaindb/models.py | 80 ++++++++++++++++++++++---------------------- 1 file changed, 40 insertions(+), 40 deletions(-) diff --git a/bigchaindb/models.py b/bigchaindb/models.py index 6d2ff053..afda5ca9 100644 --- a/bigchaindb/models.py +++ b/bigchaindb/models.py @@ -120,7 +120,24 @@ class Transaction(Transaction): class Block(object): """A Block bundles up to 1000 Transactions. Nodes vote on its validity. - Attributes: + 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. + voters (:obj:`list` of :obj:`str`): A list of a federation + nodes' public keys supposed to vote on the Block. + signature (str): A cryptographic signature ensuring the + integrity and creatorship of a Block. + """ + + def __init__(self, transactions=None, node_pubkey=None, timestamp=None, + voters=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 @@ -130,23 +147,6 @@ class Block(object): nodes' public keys supposed to vote on the Block. signature (str): A cryptographic signature ensuring the integrity and creatorship of a Block. - """ - - def __init__(self, transactions=None, node_pubkey=None, timestamp=None, - voters=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. - voters (:obj:`list` of :obj:`str`): A list of a federation - nodes' public keys supposed to vote on the Block. - signature (str): A cryptographic signature ensuring the - integrity and creatorship of a Block. """ if transactions is not None and not isinstance(transactions, list): raise TypeError('`transactions` must be a list instance or None') @@ -214,12 +214,12 @@ class Block(object): def sign(self, signing_key): """Creates a signature for the Block and overwrites `self.signature`. - Args: - signing_key (str): A signing key corresponding to - `self.node_pubkey`. + Args: + signing_key (str): A signing key corresponding to + `self.node_pubkey`. - Returns: - :class:`~.Block` + Returns: + :class:`~.Block` """ block_body = self.to_dict() block_serialized = serialize(block_body['block']) @@ -230,8 +230,8 @@ class Block(object): def is_signature_valid(self): """Checks the validity of a Block's signature. - Returns: - bool: Stating the validity of the Block's signature. + Returns: + bool: Stating the validity of the Block's signature. """ block = self.to_dict()['block'] # cc only accepts bytestring messages @@ -248,20 +248,20 @@ class Block(object): def from_dict(cls, block_body): """Transforms a Python dictionary to a Block object. - Note: - Throws errors if passed signature or id is incorrect. + Note: + Throws errors if passed signature or id is incorrect. - Args: - block_body (dict): A block dictionary to be transformed. + Args: + block_body (dict): A block dictionary to be transformed. - Returns: - :class:`~Block` + Returns: + :class:`~Block` - Raises: - InvalidHash: If the block's id is not corresponding to its - data. - InvalidSignature: If the block's signature is not corresponding - to it's data or `node_pubkey`. + Raises: + InvalidHash: If the block's id is not corresponding to its + data. + InvalidSignature: If the block's signature is not corresponding + to it's data or `node_pubkey`. """ # TODO: Reuse `is_signature_valid` method here. block = block_body['block'] @@ -301,11 +301,11 @@ class Block(object): def to_dict(self): """Transforms the object to a Python dictionary. - Returns: - dict: The Block as an alternative serialization format. + Returns: + dict: The Block as an alternative serialization format. - Raises: - OperationError: If a Block doesn't contain any transactions. + Raises: + OperationError: If a Block doesn't contain any transactions. """ if len(self.transactions) == 0: raise OperationError('Empty block creation is not allowed') From 37cf057b99e14ae49bdb9410372d106801fa6a66 Mon Sep 17 00:00:00 2001 From: tim Date: Mon, 14 Nov 2016 13:56:05 +0100 Subject: [PATCH 32/54] PR Feedback for docs --- bigchaindb/models.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/bigchaindb/models.py b/bigchaindb/models.py index afda5ca9..a6a2ecdb 100644 --- a/bigchaindb/models.py +++ b/bigchaindb/models.py @@ -248,9 +248,6 @@ class Block(object): def from_dict(cls, block_body): """Transforms a Python dictionary to a Block object. - Note: - Throws errors if passed signature or id is incorrect. - Args: block_body (dict): A block dictionary to be transformed. @@ -299,13 +296,13 @@ class Block(object): return self.to_dict()['id'] def to_dict(self): - """Transforms the object to a Python dictionary. + """Transforms the Block to a Python dictionary. Returns: - dict: The Block as an alternative serialization format. + dict: The Block as a dict. Raises: - OperationError: If a Block doesn't contain any transactions. + OperationError: If the Block doesn't contain any transactions. """ if len(self.transactions) == 0: raise OperationError('Empty block creation is not allowed') From 6ef18fbbaecfc3d4e92b2f77baaab885bd3ff2ed Mon Sep 17 00:00:00 2001 From: Sylvain Bellemare Date: Fri, 11 Nov 2016 19:21:24 +0100 Subject: [PATCH 33/54] Simplify Transaction.to_inputs method --- bigchaindb/common/transaction.py | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/bigchaindb/common/transaction.py b/bigchaindb/common/transaction.py index c1857d23..9bcb5e1f 100644 --- a/bigchaindb/common/transaction.py +++ b/bigchaindb/common/transaction.py @@ -786,20 +786,14 @@ class Transaction(object): :obj:`list` of :class:`~bigchaindb.common.transaction. Fulfillment` """ - inputs = [] - if condition_indices is None or len(condition_indices) == 0: - # NOTE: If no condition indices are passed, we just assume to - # take all conditions as inputs. - condition_indices = [index for index, _ - in enumerate(self.conditions)] - - for cid in condition_indices: - input_cond = self.conditions[cid] - ffill = Fulfillment(input_cond.fulfillment, - input_cond.owners_after, - TransactionLink(self.id, cid)) - inputs.append(ffill) - return inputs + # NOTE: If no condition indices are passed, we just assume to + # take all conditions as inputs. + return [ + Fulfillment(self.conditions[cid].fulfillment, + self.conditions[cid].owners_after, + TransactionLink(self.id, cid)) + for cid in condition_indices or range(len(self.conditions)) + ] def add_fulfillment(self, fulfillment): """Adds a Fulfillment to a Transaction's list of Fulfillments. From 74e1f9c30330058ab5fd7fe25d5742fe4d66626e Mon Sep 17 00:00:00 2001 From: tim Date: Mon, 14 Nov 2016 14:59:09 +0100 Subject: [PATCH 34/54] Fix tense in docs --- bigchaindb/models.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/bigchaindb/models.py b/bigchaindb/models.py index a6a2ecdb..57d64272 100644 --- a/bigchaindb/models.py +++ b/bigchaindb/models.py @@ -118,7 +118,7 @@ class Transaction(Transaction): class Block(object): - """A Block bundles up to 1000 Transactions. Nodes vote on its validity. + """Bundle a list of Transactions in a Block. Nodes vote on its validity. Attributes: transaction (:obj:`list` of :class:`~.Transaction`): @@ -129,7 +129,7 @@ class Block(object): voters (:obj:`list` of :obj:`str`): A list of a federation nodes' public keys supposed to vote on the Block. signature (str): A cryptographic signature ensuring the - integrity and creatorship of a Block. + integrity and validity of the creator of a Block. """ def __init__(self, transactions=None, node_pubkey=None, timestamp=None, @@ -146,7 +146,7 @@ class Block(object): voters (:obj:`list` of :obj:`str`): A list of a federation nodes' public keys supposed to vote on the Block. signature (str): A cryptographic signature ensuring the - integrity and creatorship of a Block. + 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') @@ -174,14 +174,14 @@ class Block(object): return self.to_dict() == other def validate(self, bigchain): - """Validates the Block. + """Validate the Block. Args: bigchain (:class:`~bigchaindb.Bigchain`): An instantiated Bigchain object. Returns: - :class:`~.Block`: If valid, returns a `Block` object. Else an + :class:`~.Block`: If valid, return a `Block` object. Else an appropriate exception describing the reason of invalidity is raised. @@ -212,7 +212,7 @@ class Block(object): return self def sign(self, signing_key): - """Creates a signature for the Block and overwrites `self.signature`. + """Create a signature for the Block and overwrite `self.signature`. Args: signing_key (str): A signing key corresponding to @@ -228,7 +228,7 @@ class Block(object): return self def is_signature_valid(self): - """Checks the validity of a Block's signature. + """Check the validity of a Block's signature. Returns: bool: Stating the validity of the Block's signature. @@ -246,7 +246,7 @@ class Block(object): @classmethod def from_dict(cls, block_body): - """Transforms a Python dictionary to a Block object. + """Transform a Python dictionary to a Block object. Args: block_body (dict): A block dictionary to be transformed. @@ -296,7 +296,7 @@ class Block(object): return self.to_dict()['id'] def to_dict(self): - """Transforms the Block to a Python dictionary. + """Transform the Block to a Python dictionary. Returns: dict: The Block as a dict. From e7ff6edd4e6a10efa6bca36149b79b626090efef Mon Sep 17 00:00:00 2001 From: Sylvain Bellemare Date: Mon, 14 Nov 2016 15:54:24 +0100 Subject: [PATCH 35/54] Fix typos --- bigchaindb/common/transaction.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bigchaindb/common/transaction.py b/bigchaindb/common/transaction.py index 849153af..af12fafd 100644 --- a/bigchaindb/common/transaction.py +++ b/bigchaindb/common/transaction.py @@ -292,9 +292,9 @@ class Condition(object): Returns: A Condition that can be used in a Transaction. - Returns: + Raises: TypeError: If `owners_after` is not an instance of `list`. - Value: If `owners_after` is an empty list. + ValueError: If `owners_after` is an empty list. """ threshold = len(owners_after) if not isinstance(amount, int): From 6724e64cca44651ce20a7c0ee047f32dc7c5b543 Mon Sep 17 00:00:00 2001 From: Sylvain Bellemare Date: Mon, 14 Nov 2016 16:46:12 +0100 Subject: [PATCH 36/54] Update test to divisible asset change --- tests/db/test_bigchain_api.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/db/test_bigchain_api.py b/tests/db/test_bigchain_api.py index 045149de..247a31b4 100644 --- a/tests/db/test_bigchain_api.py +++ b/tests/db/test_bigchain_api.py @@ -598,7 +598,8 @@ class TestBigchainApi(object): from bigchaindb.models import Transaction for _ in range(4): - tx = Transaction.create([b.me], [user_vk]).sign([b.me_private]) + tx = Transaction.create([b.me], + [([user_vk], 1)]).sign([b.me_private]) b.write_transaction(tx) assert b.backend.count_backlog() == 4 From d577bf8a511a1b2393d2e95825cd85ca973fe320 Mon Sep 17 00:00:00 2001 From: troymc Date: Mon, 14 Nov 2016 18:46:03 +0100 Subject: [PATCH 37/54] Fixed two flake8 errors --- bigchaindb/common/crypto.py | 1 + bigchaindb/web/views/transactions.py | 1 + 2 files changed, 2 insertions(+) diff --git a/bigchaindb/common/crypto.py b/bigchaindb/common/crypto.py index e440f81d..82b15136 100644 --- a/bigchaindb/common/crypto.py +++ b/bigchaindb/common/crypto.py @@ -14,5 +14,6 @@ def generate_key_pair(): private_key, public_key = crypto.ed25519_generate_key_pair() return private_key.decode(), public_key.decode() + SigningKey = crypto.Ed25519SigningKey VerifyingKey = crypto.Ed25519VerifyingKey diff --git a/bigchaindb/web/views/transactions.py b/bigchaindb/web/views/transactions.py index 8780fda8..c529b6b3 100644 --- a/bigchaindb/web/views/transactions.py +++ b/bigchaindb/web/views/transactions.py @@ -111,6 +111,7 @@ class TransactionListApi(Resource): return tx + transaction_api.add_resource(TransactionApi, '/transactions/', strict_slashes=False) From 196f11098bfef645fac3732b4b94dff7751f9916 Mon Sep 17 00:00:00 2001 From: Troy McConaghy Date: Mon, 14 Nov 2016 18:54:50 +0100 Subject: [PATCH 38/54] Added note about running flake8 --- PYTHON_STYLE_GUIDE.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/PYTHON_STYLE_GUIDE.md b/PYTHON_STYLE_GUIDE.md index ca94c197..a2a9800b 100644 --- a/PYTHON_STYLE_GUIDE.md +++ b/PYTHON_STYLE_GUIDE.md @@ -65,6 +65,14 @@ x = 'name: {}; score: {}'.format(name, n) we use the `format()` version. The [official Python documentation says](https://docs.python.org/2/library/stdtypes.html#str.format), "This method of string formatting is the new standard in Python 3, and should be preferred to the % formatting described in String Formatting Operations in new code." +## Runnng the Flake8 Style Checker + +We use [Flake8](http://flake8.pycqa.org/en/latest/index.html) to check our Python code style. Once you have it installed, you can run it using: +```text +flake8 --max-line-length 119 bigchaindb/ +``` + + ## Writing and Running (Python) Unit Tests We write unit tests for our Python code using the [pytest](http://pytest.org/latest/) framework. From e99930c5014eaf9f7e868a8a9d398fa68342b2b1 Mon Sep 17 00:00:00 2001 From: tim Date: Mon, 14 Nov 2016 15:18:14 +0100 Subject: [PATCH 39/54] web/test_basic_views --> web/test_transactions --- tests/web/{test_basic_views.py => test_transactions.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/web/{test_basic_views.py => test_transactions.py} (100%) diff --git a/tests/web/test_basic_views.py b/tests/web/test_transactions.py similarity index 100% rename from tests/web/test_basic_views.py rename to tests/web/test_transactions.py From 85eccb48be3ce21bfa889ac13c3ae4a6ca4c1b6c Mon Sep 17 00:00:00 2001 From: tim Date: Tue, 15 Nov 2016 11:53:41 +0100 Subject: [PATCH 40/54] Test file for info.py --- tests/web/test_info.py | 5 +++++ tests/web/test_transactions.py | 7 ------- 2 files changed, 5 insertions(+), 7 deletions(-) create mode 100644 tests/web/test_info.py diff --git a/tests/web/test_info.py b/tests/web/test_info.py new file mode 100644 index 00000000..759600c5 --- /dev/null +++ b/tests/web/test_info.py @@ -0,0 +1,5 @@ +def test_api_endpoint_shows_basic_info(client): + from bigchaindb import version + res = client.get('/') + assert res.json['software'] == 'BigchainDB' + assert res.json['version'] == version.__version__ diff --git a/tests/web/test_transactions.py b/tests/web/test_transactions.py index 7d382ca5..4a8685fc 100644 --- a/tests/web/test_transactions.py +++ b/tests/web/test_transactions.py @@ -25,13 +25,6 @@ def test_get_transaction_returns_404_if_not_found(client): assert res.status_code == 404 -def test_api_endpoint_shows_basic_info(client): - from bigchaindb import version - res = client.get('/') - assert res.json['software'] == 'BigchainDB' - assert res.json['version'] == version.__version__ - - def test_post_create_transaction_endpoint(b, client): from bigchaindb.models import Transaction user_priv, user_pub = crypto.generate_key_pair() From 74b1662a78b3ddfd750001554fc7dee75ff53619 Mon Sep 17 00:00:00 2001 From: tim Date: Thu, 10 Nov 2016 11:02:23 +0100 Subject: [PATCH 41/54] Format http api docs to 79 chars --- docs/server/source/drivers-clients/http-client-server-api.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/server/source/drivers-clients/http-client-server-api.rst b/docs/server/source/drivers-clients/http-client-server-api.rst index d274efad..bf0033b6 100644 --- a/docs/server/source/drivers-clients/http-client-server-api.rst +++ b/docs/server/source/drivers-clients/http-client-server-api.rst @@ -11,7 +11,7 @@ When you start Bigchaindb using `bigchaindb start`, an HTTP API is exposed at the address stored in the BigchainDB node configuration settings. The default is: -`http://localhost:9984/api/v1/ `_ +`http://localhost:9984/api/v1/ ` but that address can be changed by changing the "API endpoint" configuration setting (e.g. in a local config file). There's more information about setting From 93f3e9a06b1862115b29a832577aac6a69f154db Mon Sep 17 00:00:00 2001 From: tim Date: Thu, 10 Nov 2016 12:07:52 +0100 Subject: [PATCH 42/54] Document unspents endpoint --- .../http-client-server-api.rst | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/docs/server/source/drivers-clients/http-client-server-api.rst b/docs/server/source/drivers-clients/http-client-server-api.rst index bf0033b6..2db895ad 100644 --- a/docs/server/source/drivers-clients/http-client-server-api.rst +++ b/docs/server/source/drivers-clients/http-client-server-api.rst @@ -283,3 +283,54 @@ GET /transactions/{tx_id} :statuscode 200: A transaction with that ID was found. :statuscode 404: A transaction with that ID was not found. + + +GET /unspents/ +------------------------- + +.. http:get:: /unspents?owner_after={owner_after} + + Get a list of links to transactions' conditions that have not been used in + a previous transaction and could hence be called unspent conditions/outputs + (or simply: unspents). + + This endpoint doesn't return anything if the querystring ``owner_after`` + happens to not be defined in the request. + + Note that if unspents for a certain ``owner_after`` have not been found by + the server, this will result in the server returning a 200 OK HTTP status + code and an empty list in the response's body. + + :param owner_after: A public key, able to validly spend an output of a + transaction, assuming the user also has the corresponding private key. + :type owner_after: hex string + + **Example request**: + + .. sourcecode:: http + + GET /unspents?owner_after=1AAAbbb...ccc HTTP/1.1 + Host: example.com + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/json + + [ + { + "txid": "2d431073e1477f3073a4693ac7ff9be5634751de1b8abaa1f4e19548ef0b4b0e", + "cid": 0 + }, + { + "txid": "2d431073e1477f3073a4693ac7ff9be5634751de1b8abaa1f4e19548ef0b4b0e", + "cid": 1 + }, + ] + + :statuscode 200: A list of outputs were found and returned in the body of + the response. + :statuscode 400: The request wasn't understood by the server, e.g. + the ``owner_after`` querystring was not included in the request. From 284411d881d77df1a33d2af01ad59bc396496fa8 Mon Sep 17 00:00:00 2001 From: tim Date: Mon, 14 Nov 2016 13:38:34 +0100 Subject: [PATCH 43/54] PR Feedback --- .../source/drivers-clients/http-client-server-api.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/server/source/drivers-clients/http-client-server-api.rst b/docs/server/source/drivers-clients/http-client-server-api.rst index 2db895ad..bf567a51 100644 --- a/docs/server/source/drivers-clients/http-client-server-api.rst +++ b/docs/server/source/drivers-clients/http-client-server-api.rst @@ -11,7 +11,7 @@ When you start Bigchaindb using `bigchaindb start`, an HTTP API is exposed at the address stored in the BigchainDB node configuration settings. The default is: -`http://localhost:9984/api/v1/ ` +`http://localhost:9984/api/v1/ `_ but that address can be changed by changing the "API endpoint" configuration setting (e.g. in a local config file). There's more information about setting @@ -294,8 +294,8 @@ GET /unspents/ a previous transaction and could hence be called unspent conditions/outputs (or simply: unspents). - This endpoint doesn't return anything if the querystring ``owner_after`` - happens to not be defined in the request. + This endpoint will return a ``HTTP 400 Bad Request`` if the querystring + ``owner_after`` happens to not be defined in the request. Note that if unspents for a certain ``owner_after`` have not been found by the server, this will result in the server returning a 200 OK HTTP status @@ -303,7 +303,7 @@ GET /unspents/ :param owner_after: A public key, able to validly spend an output of a transaction, assuming the user also has the corresponding private key. - :type owner_after: hex string + :type owner_after: base58 encoded string **Example request**: From f82f1add035a75ca034f2a507f125e17e125e2e1 Mon Sep 17 00:00:00 2001 From: tim Date: Tue, 15 Nov 2016 11:43:54 +0100 Subject: [PATCH 44/54] TransactionLinks --> URIs --- .../source/drivers-clients/http-client-server-api.rst | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/docs/server/source/drivers-clients/http-client-server-api.rst b/docs/server/source/drivers-clients/http-client-server-api.rst index bf567a51..98ceddd1 100644 --- a/docs/server/source/drivers-clients/http-client-server-api.rst +++ b/docs/server/source/drivers-clients/http-client-server-api.rst @@ -320,14 +320,9 @@ GET /unspents/ Content-Type: application/json [ - { - "txid": "2d431073e1477f3073a4693ac7ff9be5634751de1b8abaa1f4e19548ef0b4b0e", - "cid": 0 - }, - { - "txid": "2d431073e1477f3073a4693ac7ff9be5634751de1b8abaa1f4e19548ef0b4b0e", - "cid": 1 - }, + + '../transactions/2d431073e1477f3073a4693ac7ff9be5634751de1b8abaa1f4e19548ef0b4b0e/conditions/0', + '../transactions/2d431073e1477f3073a4693ac7ff9be5634751de1b8abaa1f4e19548ef0b4b0e/conditions/1' ] :statuscode 200: A list of outputs were found and returned in the body of From 5ef532976d2a9c6a4319979d9fd6420b9e315c37 Mon Sep 17 00:00:00 2001 From: tim Date: Tue, 15 Nov 2016 13:46:39 +0100 Subject: [PATCH 45/54] Corrections to render sphinx correctly --- .../source/drivers-clients/http-client-server-api.rst | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/docs/server/source/drivers-clients/http-client-server-api.rst b/docs/server/source/drivers-clients/http-client-server-api.rst index 98ceddd1..f50b4f98 100644 --- a/docs/server/source/drivers-clients/http-client-server-api.rst +++ b/docs/server/source/drivers-clients/http-client-server-api.rst @@ -301,8 +301,7 @@ GET /unspents/ the server, this will result in the server returning a 200 OK HTTP status code and an empty list in the response's body. - :param owner_after: A public key, able to validly spend an output of a - transaction, assuming the user also has the corresponding private key. + :param owner_after: A public key, able to validly spend an output of a transaction, assuming the user also has the corresponding private key. :type owner_after: base58 encoded string **Example request**: @@ -320,12 +319,9 @@ GET /unspents/ Content-Type: application/json [ - '../transactions/2d431073e1477f3073a4693ac7ff9be5634751de1b8abaa1f4e19548ef0b4b0e/conditions/0', '../transactions/2d431073e1477f3073a4693ac7ff9be5634751de1b8abaa1f4e19548ef0b4b0e/conditions/1' ] - :statuscode 200: A list of outputs were found and returned in the body of - the response. - :statuscode 400: The request wasn't understood by the server, e.g. - the ``owner_after`` querystring was not included in the request. + :statuscode 200: A list of outputs were found and returned in the body of the response. + :statuscode 400: The request wasn't understood by the server, e.g. the ``owner_after`` querystring was not included in the request. From 786635df4a13b922079590ad6d1b9f03f73ba39c Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Tue, 15 Nov 2016 17:41:35 +0100 Subject: [PATCH 46/54] Explicitly pass settings for flask into flask app factory (#750) --- bigchaindb/web/server.py | 12 ++++++++---- tests/web/conftest.py | 2 +- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/bigchaindb/web/server.py b/bigchaindb/web/server.py index 50ef4cc3..1756fffe 100644 --- a/bigchaindb/web/server.py +++ b/bigchaindb/web/server.py @@ -49,19 +49,22 @@ class StandaloneApplication(gunicorn.app.base.BaseApplication): return self.application -def create_app(settings): +def create_app(*, debug=False, threads=4): """Return an instance of the Flask application. Args: debug (bool): a flag to activate the debug mode for the app (default: False). + threads (int): number of threads to use + Return: + an instance of the Flask application. """ app = Flask(__name__) - app.debug = settings.get('debug', False) + app.debug = debug - app.config['bigchain_pool'] = util.pool(Bigchain, size=settings.get('threads', 4)) + app.config['bigchain_pool'] = util.pool(Bigchain, size=threads) app.config['monitor'] = Monitor() app.register_blueprint(info_views, url_prefix='/') @@ -88,6 +91,7 @@ def create_server(settings): if not settings.get('threads'): settings['threads'] = (multiprocessing.cpu_count() * 2) + 1 - app = create_app(settings) + app = create_app(debug=settings.get('debug', False), + threads=settings['threads']) standalone = StandaloneApplication(app, settings) return standalone diff --git a/tests/web/conftest.py b/tests/web/conftest.py index db5583e7..95874f5c 100644 --- a/tests/web/conftest.py +++ b/tests/web/conftest.py @@ -25,7 +25,7 @@ def app(request, node_config): restore_config(request, node_config) from bigchaindb.web import server - app = server.create_app({'debug': True}) + app = server.create_app(debug=True) return app From 65a54470a623a20520a8a316b5d58a7e665eb96f Mon Sep 17 00:00:00 2001 From: troymc Date: Wed, 16 Nov 2016 09:52:18 +0100 Subject: [PATCH 47/54] Updated top docstring of commands/bigchain.py --- bigchaindb/commands/bigchain.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/bigchaindb/commands/bigchain.py b/bigchaindb/commands/bigchain.py index d52131ab..a70ff65c 100644 --- a/bigchaindb/commands/bigchain.py +++ b/bigchaindb/commands/bigchain.py @@ -1,6 +1,5 @@ """Implementation of the `bigchaindb` command, -which is one of the commands in the BigchainDB -command-line interface. +the command-line interface (CLI) for BigchainDB Server. """ import os From 2f6e8abac22fd387a2d455a8ff21f63c94d6b3be Mon Sep 17 00:00:00 2001 From: troymc Date: Wed, 16 Nov 2016 10:10:09 +0100 Subject: [PATCH 48/54] Fixed the call to Transaction.create(): 2nd arg a list of tuples --- bigchaindb/commands/bigchain.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bigchaindb/commands/bigchain.py b/bigchaindb/commands/bigchain.py index a70ff65c..217aeeb1 100644 --- a/bigchaindb/commands/bigchain.py +++ b/bigchaindb/commands/bigchain.py @@ -193,7 +193,7 @@ def _run_load(tx_left, stats): b = bigchaindb.Bigchain() while True: - tx = Transaction.create([b.me], [b.me]) + tx = Transaction.create([b.me], [([b.me], 1)]) tx = tx.sign([b.me_private]) b.write_transaction(tx) From eb362fd6e9f1d3de24f427dd4e0bb665a48a7034 Mon Sep 17 00:00:00 2001 From: Brett Sun Date: Wed, 16 Nov 2016 11:21:25 +0100 Subject: [PATCH 49/54] Fix equality check for AssetLinks (#825) --- bigchaindb/common/transaction.py | 2 +- tests/common/test_transaction.py | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/bigchaindb/common/transaction.py b/bigchaindb/common/transaction.py index c87b9864..74a781f3 100644 --- a/bigchaindb/common/transaction.py +++ b/bigchaindb/common/transaction.py @@ -541,7 +541,7 @@ class AssetLink(Asset): def __eq__(self, other): return isinstance(other, AssetLink) and \ - self.to_dict() == self.to_dict() + self.to_dict() == other.to_dict() @classmethod def from_dict(cls, link): diff --git a/tests/common/test_transaction.py b/tests/common/test_transaction.py index baba35af..2675ca07 100644 --- a/tests/common/test_transaction.py +++ b/tests/common/test_transaction.py @@ -516,6 +516,16 @@ def test_cast_asset_link_to_boolean(): assert bool(AssetLink(False)) is True +def test_eq_asset_link(): + from bigchaindb.common.transaction import AssetLink + + asset_id_1 = 'asset_1' + asset_id_2 = 'asset_2' + + assert AssetLink(asset_id_1) == AssetLink(asset_id_1) + assert AssetLink(asset_id_1) != AssetLink(asset_id_2) + + def test_add_fulfillment_to_tx(user_ffill): from bigchaindb.common.transaction import Transaction, Asset From 81250e60594b17c75d8bda9856c10a726f26f1f5 Mon Sep 17 00:00:00 2001 From: Troy McConaghy Date: Wed, 16 Nov 2016 14:24:05 +0100 Subject: [PATCH 50/54] Docs/826/direct link from server docs to py driver docs (#827) * Removed python-driver.md page in server docs * In server docs, changed Py Driver docs link to a direct link --- docs/server/source/drivers-clients/index.rst | 2 +- docs/server/source/drivers-clients/python-driver.md | 7 ------- 2 files changed, 1 insertion(+), 8 deletions(-) delete mode 100644 docs/server/source/drivers-clients/python-driver.md diff --git a/docs/server/source/drivers-clients/index.rst b/docs/server/source/drivers-clients/index.rst index 1c55d133..cb749788 100644 --- a/docs/server/source/drivers-clients/index.rst +++ b/docs/server/source/drivers-clients/index.rst @@ -5,6 +5,6 @@ Drivers & Clients :maxdepth: 1 http-client-server-api - python-driver + The Python Driver example-apps \ No newline at end of file diff --git a/docs/server/source/drivers-clients/python-driver.md b/docs/server/source/drivers-clients/python-driver.md deleted file mode 100644 index 99563924..00000000 --- a/docs/server/source/drivers-clients/python-driver.md +++ /dev/null @@ -1,7 +0,0 @@ -# The Python Driver - -The BigchainDB Python Driver is a Python wrapper around the [HTTP Client-Server API](http-client-server-api.html). A developer can use it to develop a Python app that communicates with one or more BigchainDB clusters. - -The BigchainDB Python Driver documentation is at: - -[http://docs.bigchaindb.com/projects/py-driver/en/latest/index.html](http://docs.bigchaindb.com/projects/py-driver/en/latest/index.html) From 1ead2ea0f1f00dcdf69bfecfab010d92af7ccb32 Mon Sep 17 00:00:00 2001 From: Scott Sadler Date: Mon, 14 Nov 2016 15:50:27 +0100 Subject: [PATCH 51/54] add a button to the docs linking to the bdb cli --- docs/root/source/index.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/root/source/index.rst b/docs/root/source/index.rst index 39235cad..c8ddb1ff 100644 --- a/docs/root/source/index.rst +++ b/docs/root/source/index.rst @@ -58,6 +58,9 @@ At a high level, one can communicate with a BigchainDB cluster (set of nodes) us + From 44e80ce2a07db273e0d8c45202d2e101f3f9f484 Mon Sep 17 00:00:00 2001 From: Scott Sadler Date: Tue, 15 Nov 2016 13:14:37 +0100 Subject: [PATCH 52/54] link to CLI from drivers and clients section of documentation --- docs/server/source/drivers-clients/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/server/source/drivers-clients/index.rst b/docs/server/source/drivers-clients/index.rst index cb749788..c36d0078 100644 --- a/docs/server/source/drivers-clients/index.rst +++ b/docs/server/source/drivers-clients/index.rst @@ -6,5 +6,5 @@ Drivers & Clients http-client-server-api The Python Driver + Transaction CLI example-apps - \ No newline at end of file From 9951b61076815b6db19f03589570e7f41351a03b Mon Sep 17 00:00:00 2001 From: Scott Sadler Date: Tue, 15 Nov 2016 13:20:59 +0100 Subject: [PATCH 53/54] add some explanation to the client-drivers documentation page. --- docs/server/source/drivers-clients/index.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/server/source/drivers-clients/index.rst b/docs/server/source/drivers-clients/index.rst index c36d0078..bef13839 100644 --- a/docs/server/source/drivers-clients/index.rst +++ b/docs/server/source/drivers-clients/index.rst @@ -1,6 +1,13 @@ Drivers & Clients ================= +Currently, the only language-native driver is written in the Python language. + +We also provide the Transaction CLI to be able to script the building of +transactions. You may be able to wrap this tool inside the language of +your choice, and then use the server api directly to post transactions. + + .. toctree:: :maxdepth: 1 From 9b4252a6c9b7044c078a6a4480ed585faa0a992c Mon Sep 17 00:00:00 2001 From: Scott Sadler Date: Wed, 16 Nov 2016 14:43:40 +0100 Subject: [PATCH 54/54] wording change to drivers&clients doc as requested by @ttmc --- docs/server/source/drivers-clients/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/server/source/drivers-clients/index.rst b/docs/server/source/drivers-clients/index.rst index bef13839..9eb81f6c 100644 --- a/docs/server/source/drivers-clients/index.rst +++ b/docs/server/source/drivers-clients/index.rst @@ -5,7 +5,7 @@ Currently, the only language-native driver is written in the Python language. We also provide the Transaction CLI to be able to script the building of transactions. You may be able to wrap this tool inside the language of -your choice, and then use the server api directly to post transactions. +your choice, and then use the HTTP API directly to post transactions. .. toctree::