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`. 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. diff --git a/Release_Process.md b/Release_Process.md new file mode 100644 index 00000000..bb826ae9 --- /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 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 +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). diff --git a/bigchaindb/commands/bigchain.py b/bigchaindb/commands/bigchain.py index d52131ab..217aeeb1 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 @@ -194,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) diff --git a/bigchaindb/common/crypto.py b/bigchaindb/common/crypto.py index a0b5a71d..acce02d9 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() + PrivateKey = crypto.Ed25519SigningKey PublicKey = crypto.Ed25519VerifyingKey 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/common/transaction.py b/bigchaindb/common/transaction.py index c55ab83e..18a50640 100644 --- a/bigchaindb/common/transaction.py +++ b/bigchaindb/common/transaction.py @@ -3,13 +3,13 @@ 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 PrivateKey, hash_data from bigchaindb.common.exceptions import (KeypairMismatchException, - InvalidHash, InvalidSignature) + InvalidHash, InvalidSignature, + AmountError, AssetIdMismatch) from bigchaindb.common.util import serialize, gen_timestamp @@ -96,6 +96,14 @@ class Fulfillment(object): ffill['fid'] = fid return ffill + @classmethod + def generate(cls, owners_before): + # TODO: write docstring + # 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): """Transforms a Python dictionary to a Fulfillment object. @@ -265,7 +273,7 @@ class Condition(object): return cond @classmethod - def generate(cls, owners_after): + def generate(cls, owners_after, amount): """Generates a Condition from a specifically formed tuple or list. Note: @@ -275,34 +283,24 @@ 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: + Raises: TypeError: If `owners_after` is not an instance of `list`. - TypeError: If `owners_after` is an empty list. + ValueError: 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 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: @@ -313,12 +311,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_after, amount=amount) @classmethod def _gen_condition(cls, initial, current): @@ -338,14 +336,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) @@ -420,7 +415,7 @@ class Asset(object): self.updatable = updatable self.refillable = refillable - self._validate_asset() + self.validate_asset() def __eq__(self, other): try: @@ -463,7 +458,38 @@ class Asset(object): """Generates a unqiue uuid for an Asset""" return str(uuid4()) - def _validate_asset(self): + @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 (: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. + + 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 all transactions passed' + ' 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): raise TypeError('`data` must be a dict instance or None') @@ -474,6 +500,77 @@ 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 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() == other.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.""" @@ -612,9 +709,17 @@ class Transaction(object): self.fulfillments = fulfillments if fulfillments 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 + # 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) + @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): """A simple way to generate a `CREATE` transaction. Note: @@ -622,7 +727,6 @@ class Transaction(object): use cases: - Ed25519 - ThresholdSha256 - - PreimageSha256. Additionally, it provides support for the following BigchainDB use cases: @@ -637,10 +741,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` @@ -649,54 +749,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: + raise ValueError('`owners_before` list cannot be empty') + if len(owners_after) == 0: + raise ValueError('`owners_after` list cannot be empty') metadata = Metadata(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) - return cls(cls.CREATE, asset, [ffill_tx], [cond_tx], metadata) + fulfillments = [] + conditions = [] - 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) for owners in owners_after] - return cls(cls.CREATE, asset, ffills, conds, metadata) + # 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 must be a' + ' tuple of `([],' + ' )`')) + pub_keys, amount = owner_after + conditions.append(Condition.generate(pub_keys, amount)) - elif len(owners_before) == 1 and len(owners_after) > 1: - # NOTE: Multiple owners case - cond_tx = Condition.generate(owners_after) - ffill = Ed25519Fulfillment(public_key=owners_before[0]) - ffill_tx = Fulfillment(ffill, owners_before) - return cls(cls.CREATE, asset, [ffill_tx], [cond_tx], metadata) + # generate fulfillments + fulfillments.append(Fulfillment.generate(owners_before)) - 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) - 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 ;)") + return cls(cls.CREATE, asset, fulfillments, conditions, metadata) @classmethod def transfer(cls, inputs, owners_after, asset, metadata=None): @@ -743,17 +817,17 @@ 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') + if len(owners_after) == 0: + raise ValueError('`owners_after` list cannot be empty') - # 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") + 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 + conditions.append(Condition.generate(pub_keys, amount)) metadata = Metadata(metadata) inputs = deepcopy(inputs) @@ -786,20 +860,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. @@ -868,13 +936,12 @@ class Transaction(object): key_pairs = {gen_public_key(PrivateKey(private_key)): PrivateKey(private_key) for private_key in private_keys} - zippedIO = enumerate(zip(self.fulfillments, self.conditions)) - for index, (fulfillment, condition) in zippedIO: + 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() @@ -1031,14 +1098,13 @@ 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 Transactions. """ 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) @@ -1049,14 +1115,13 @@ class Transaction(object): tx_serialized, input_condition_uri) - if not fulfillments_count == conditions_count == \ - input_condition_uris_count: - raise ValueError('Fulfillments, conditions and ' + 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) + + partial_transactions = map(gen_tx, self.fulfillments, + self.conditions, input_condition_uris) + return all(partial_transactions) @staticmethod def _fulfillment_valid(fulfillment, operation, tx_serialized, @@ -1214,7 +1279,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/bigchaindb/core.py b/bigchaindb/core.py index 4df29dd1..a7be4983 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 +from bigchaindb.common.transaction import TransactionLink, Asset import bigchaindb @@ -182,12 +182,35 @@ 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.TransactionNotInValidBlock, exceptions.AmountError): 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) @@ -343,6 +366,21 @@ class Bigchain(object): cursor = self.backend.get_transactions_by_asset_id(asset_id) 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.backend.get_asset_by_id(asset_id) + cursor = list(cursor) + 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. @@ -511,7 +549,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/bigchaindb/db/backends/rethinkdb.py b/bigchaindb/db/backends/rethinkdb.py index 22937dd2..b9355b41 100644 --- a/bigchaindb/db/backends/rethinkdb.py +++ b/bigchaindb/db/backends/rethinkdb.py @@ -178,7 +178,27 @@ 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. + + 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. @@ -258,6 +278,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. @@ -282,6 +313,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. @@ -295,6 +337,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. @@ -319,10 +372,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 diff --git a/bigchaindb/models.py b/bigchaindb/models.py index 5aa697cb..3bbc80e3 100644 --- a/bigchaindb/models.py +++ b/bigchaindb/models.py @@ -2,42 +2,12 @@ from bigchaindb.common.crypto import hash_data, PublicKey, PrivateKey from bigchaindb.common.exceptions import (InvalidHash, InvalidSignature, OperationError, DoubleSpend, TransactionDoesNotExist, - FulfillmentNotInValidBlock, - AssetIdMismatch) + TransactionNotInValidBlock, + AssetIdMismatch, AmountError) 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. @@ -54,6 +24,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 @@ -71,7 +43,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 ' @@ -79,6 +52,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 @@ -90,7 +64,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)) @@ -101,11 +75,34 @@ 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 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 = 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' + ' in the outputs `{}`') + .format(input_amount, output_amount)) + else: allowed_operations = ', '.join(Transaction.ALLOWED_OPERATIONS) raise TypeError('`operation`: `{}` must be either {}.' @@ -118,8 +115,36 @@ class Transaction(Transaction): class Block(object): + """Bundle a list of Transactions in a Block. 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 validity of the creator 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 validity of the creator of a Block. + """ if transactions is not None and not isinstance(transactions, list): raise TypeError('`transactions` must be a list instance or None') else: @@ -146,18 +171,20 @@ class Block(object): return self.to_dict() == other def validate(self, bigchain): - """Validate a block. + """Validate 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, return 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 +209,15 @@ class Block(object): return self def sign(self, private_key): + """Create a signature for the Block and overwrite `self.signature`. + + Args: + private_key (str): A private key corresponding to + `self.node_pubkey`. + + Returns: + :class:`~.Block` + """ block_body = self.to_dict() block_serialized = serialize(block_body['block']) private_key = PrivateKey(private_key) @@ -189,8 +225,13 @@ class Block(object): return self def is_signature_valid(self): + """Check the validity of a Block's signature. + + Returns: + bool: Stating the validity of the Block's signature. + """ block = self.to_dict()['block'] - # cc only accepts bytesting messages + # cc only accepts bytestring messages block_serialized = serialize(block).encode() public_key = PublicKey(block['node_pubkey']) try: @@ -202,6 +243,21 @@ class Block(object): @classmethod def from_dict(cls, block_body): + """Transform a Python dictionary to a Block object. + + 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) @@ -237,6 +293,14 @@ class Block(object): return self.to_dict()['id'] def to_dict(self): + """Transform the Block to a Python dictionary. + + Returns: + dict: The Block as a dict. + + Raises: + OperationError: If the Block doesn't contain any transactions. + """ if len(self.transactions) == 0: raise OperationError('Empty block creation is not allowed') diff --git a/bigchaindb/pipelines/vote.py b/bigchaindb/pipelines/vote.py index 6b12f55b..b89e0786 100644 --- a/bigchaindb/pipelines/vote.py +++ b/bigchaindb/pipelines/vote.py @@ -40,10 +40,11 @@ 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']): + 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/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/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) 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 + 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..f50b4f98 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,46 @@ 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. + + +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 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 + 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: base58 encoded 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 + + [ + '../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. diff --git a/docs/server/source/drivers-clients/index.rst b/docs/server/source/drivers-clients/index.rst index 1c55d133..9eb81f6c 100644 --- a/docs/server/source/drivers-clients/index.rst +++ b/docs/server/source/drivers-clients/index.rst @@ -1,10 +1,17 @@ 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 HTTP API directly to post transactions. + + .. toctree:: :maxdepth: 1 http-client-server-api - python-driver + The Python Driver + Transaction CLI 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) diff --git a/tests/assets/test_digital_assets.py b/tests/assets/test_digital_assets.py index 2d0c0a0a..1b0a258b 100644 --- a/tests/assets/test_digital_assets.py +++ b/tests/assets/test_digital_assets.py @@ -1,5 +1,7 @@ import pytest -from ..db.conftest import inputs +from unittest.mock import patch + +from ..db.conftest import inputs # noqa @pytest.mark.usefixtures('inputs') @@ -9,7 +11,7 @@ def test_asset_transfer(b, user_pk, user_sk): tx_input = b.get_owned_ids(user_pk).pop() tx_create = b.get_transaction(tx_input.txid) - tx_transfer = Transaction.transfer(tx_create.to_inputs(), [user_pk], + tx_transfer = Transaction.transfer(tx_create.to_inputs(), [([user_pk], 1)], tx_create.asset) tx_transfer_signed = tx_transfer.sign([user_sk]) @@ -18,61 +20,40 @@ def test_asset_transfer(b, user_pk, user_sk): def test_validate_bad_asset_creation(b, user_pk): - from bigchaindb.models import Transaction + from bigchaindb.models import Transaction, Asset # `divisible` needs to be a boolean - tx = Transaction.create([b.me], [user_pk]) + tx = Transaction.create([b.me], [([user_pk], 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_pk]) + tx = Transaction.create([b.me], [([user_pk], 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_pk]) + tx = Transaction.create([b.me], [([user_pk], 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_pk]) + tx = Transaction.create([b.me], [([user_pk], 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_pk, 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_pk, 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_pk, 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_pk, user_sk): @@ -81,7 +62,7 @@ def test_validate_transfer_asset_id_mismatch(b, user_pk, user_sk): tx_create = b.get_owned_ids(user_pk).pop() tx_create = b.get_transaction(tx_create.txid) - tx_transfer = Transaction.transfer(tx_create.to_inputs(), [user_pk], + tx_transfer = Transaction.transfer(tx_create.to_inputs(), [([user_pk], 1)], tx_create.asset) tx_transfer.asset.data_id = 'aaa' tx_transfer_signed = tx_transfer.sign([user_sk]) @@ -92,7 +73,7 @@ def test_validate_transfer_asset_id_mismatch(b, user_pk, user_sk): def test_get_asset_id_create_transaction(b, user_pk): from bigchaindb.models import Transaction, Asset - tx_create = Transaction.create([b.me], [user_pk]) + tx_create = Transaction.create([b.me], [([user_pk], 1)]) asset_id = Asset.get_asset_id(tx_create) assert asset_id == tx_create.asset.data_id @@ -105,7 +86,7 @@ def test_get_asset_id_transfer_transaction(b, user_pk, user_sk): tx_create = b.get_owned_ids(user_pk).pop() tx_create = b.get_transaction(tx_create.txid) # create a transfer transaction - tx_transfer = Transaction.transfer(tx_create.to_inputs(), [user_pk], + tx_transfer = Transaction.transfer(tx_create.to_inputs(), [([user_pk], 1)], tx_create.asset) tx_transfer_signed = tx_transfer.sign([user_sk]) # create a block @@ -123,8 +104,8 @@ def test_asset_id_mismatch(b, user_pk): from bigchaindb.models import Transaction, Asset from bigchaindb.common.exceptions import AssetIdMismatch - tx1 = Transaction.create([b.me], [user_pk]) - tx2 = Transaction.create([b.me], [user_pk]) + tx1 = Transaction.create([b.me], [([user_pk], 1)]) + tx2 = Transaction.create([b.me], [([user_pk], 1)]) with pytest.raises(AssetIdMismatch): Asset.get_asset_id([tx1, tx2]) @@ -144,7 +125,7 @@ def test_get_txs_by_asset_id(b, user_pk, user_sk): assert txs[0].asset.data_id == asset_id # create a transfer transaction - tx_transfer = Transaction.transfer(tx_create.to_inputs(), [user_pk], + tx_transfer = Transaction.transfer(tx_create.to_inputs(), [([user_pk], 1)], tx_create.asset) tx_transfer_signed = tx_transfer.sign([user_sk]) # create the block @@ -161,3 +142,73 @@ def test_get_txs_by_asset_id(b, user_pk, 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 + + +@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 + + # 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], 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], 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], 2)], asset=asset) + 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], 1)], asset=asset) + 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], 2)], asset=asset) + tx_signed = tx.sign([user_sk]) + assert b.is_valid_transaction(tx_signed) diff --git a/tests/assets/test_divisible_assets.py b/tests/assets/test_divisible_assets.py new file mode 100644 index 00000000..5ae360e0 --- /dev/null +++ b/tests/assets/test_divisible_assets.py @@ -0,0 +1,779 @@ +import pytest + +from unittest.mock import patch + +from ..db.conftest import inputs # noqa + + +# 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 owners_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 +# 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 + 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, 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 +# 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 + + +# 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 + + +# 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(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 + + # 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 + + +@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 + + +# 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) diff --git a/tests/common/test_asset.py b/tests/common/test_asset.py index edfbcb5f..f6a3f89d 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') diff --git a/tests/common/test_transaction.py b/tests/common/test_transaction.py index 1a59d08b..2839ee3c 100644 --- a/tests/common/test_transaction.py +++ b/tests/common/test_transaction.py @@ -1,4 +1,5 @@ -from pytest import raises, mark +from pytest import raises +from unittest.mock import patch def test_fulfillment_serialization(ffill_uri, user_pub): @@ -166,29 +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]]) - 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)) + cond = Condition.generate([user_pub, [user2_pub, expected_simple3]], 1) assert cond.fulfillment.to_dict() == expected.to_dict() @@ -208,7 +187,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 +204,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 +213,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,48 +223,23 @@ 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() -# 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]) - 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 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 +275,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 @@ -510,10 +465,72 @@ 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_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 - 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 +539,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 +548,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 +558,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') @@ -614,16 +634,14 @@ def test_validate_multiple_fulfillments(user_ffill, user_cond, user_priv): from bigchaindb.common.crypto import PrivateKey from bigchaindb.common.transaction import Transaction, Asset - 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_second.fulfillments = [expected_second.fulfillments[1]] - expected_second.conditions = [expected_second.conditions[1]] expected_first_bytes = str(expected_first).encode() expected_first.fulfillments[0].fulfillment.sign(expected_first_bytes, @@ -674,7 +692,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]) @@ -715,10 +733,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]]) - 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, @@ -754,7 +768,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,17 +781,20 @@ 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 -@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)], @@ -785,45 +803,32 @@ 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 -@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], - {'message': 'hello'}) + 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 @@ -862,7 +867,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') @@ -876,80 +882,27 @@ 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 -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 - - -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(): +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): @@ -995,7 +948,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'] @@ -1014,16 +967,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': { @@ -1037,7 +989,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 } }, { @@ -1047,8 +999,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 } } ], @@ -1056,30 +1008,28 @@ 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 - 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): @@ -1090,17 +1040,25 @@ 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(): - 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/conftest.py b/tests/conftest.py index 7671cba1..957a5698 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_pk): from bigchaindb.models import Transaction - return Transaction.create([b.me], [user_pk]) + return Transaction.create([b.me], [([user_pk], 1)]) @pytest.fixture @@ -84,5 +84,5 @@ def signed_create_tx(b, create_tx): def signed_transfer_tx(signed_create_tx, user_pk, user_sk): from bigchaindb.models import Transaction inputs = signed_create_tx.to_inputs() - tx = Transaction.transfer(inputs, [user_pk], signed_create_tx.asset) + tx = Transaction.transfer(inputs, [([user_pk], 1)], signed_create_tx.asset) return tx.sign([user_sk]) diff --git a/tests/db/conftest.py b/tests/db/conftest.py index 219cf068..b04e0a39 100644 --- a/tests/db/conftest.py +++ b/tests/db/conftest.py @@ -84,7 +84,7 @@ def inputs(user_pk): prev_block_id = g.id for block in range(4): transactions = [ - Transaction.create([b.me], [user_pk]).sign([b.me_private]) + Transaction.create([b.me], [([user_pk], 1)]).sign([b.me_private]) for i in range(10) ] block = b.create_block(transactions) diff --git a/tests/db/test_bigchain_api.py b/tests/db/test_bigchain_api.py index 1d40ae77..3d1c6151 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_pk], metadata=metadata) + tx = Transaction.create([b.me], [([user_pk], 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_pk).pop() input_tx = b.get_transaction(input_tx.txid) inputs = input_tx.to_inputs() - tx = Transaction.transfer(inputs, [user_pk], input_tx.asset) + tx = Transaction.transfer(inputs, [([user_pk], 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_pk).pop() input_tx = b.get_transaction(input_tx.txid) inputs = input_tx.to_inputs() - tx = Transaction.transfer(inputs, [user_pk], input_tx.asset) + tx = Transaction.transfer(inputs, [([user_pk], 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_pk).pop() input_tx = b.get_transaction(input_tx.txid) inputs = input_tx.to_inputs() - tx = Transaction.transfer(inputs, [user_pk], input_tx.asset) + tx = Transaction.transfer(inputs, [([user_pk], 1)], input_tx.asset) tx = tx.sign([user_sk]) # There's no need to b.write_transaction(tx) to the backlog @@ -267,7 +269,7 @@ class TestBigchainApi(object): input_tx = b.get_owned_ids(user_pk).pop() input_tx = b.get_transaction(input_tx.txid) inputs = input_tx.to_inputs() - tx = Transaction.transfer(inputs, [user_pk], input_tx.asset) + tx = Transaction.transfer(inputs, [([user_pk], 1)], input_tx.asset) tx = tx.sign([user_sk]) # Make sure there's a copy of tx in the backlog @@ -369,6 +371,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 @@ -528,7 +539,7 @@ class TestBigchainApi(object): input_tx = b.get_owned_ids(user_pk).pop() input_tx = b.get_transaction(input_tx.txid) inputs = input_tx.to_inputs() - tx = Transaction.transfer(inputs, [user_pk], input_tx.asset) + tx = Transaction.transfer(inputs, [([user_pk], 1)], input_tx.asset) tx = tx.sign([user_sk]) b.write_transaction(tx) @@ -554,7 +565,7 @@ class TestBigchainApi(object): input_tx = b.get_owned_ids(user_pk).pop() input_tx = b.get_transaction(input_tx.txid) inputs = input_tx.to_inputs() - tx = Transaction.transfer(inputs, [user_pk], input_tx.asset) + tx = Transaction.transfer(inputs, [([user_pk], 1)], input_tx.asset) tx = tx.sign([user_sk]) b.write_transaction(tx) @@ -578,11 +589,21 @@ class TestBigchainApi(object): fulfillment = Fulfillment(Ed25519Fulfillment(public_key=user_pk), [user_pk], TransactionLink('somethingsomething', 0)) - tx = Transaction.transfer([fulfillment], [user_pk], Asset()) + tx = Transaction.transfer([fulfillment], [([user_pk], 1)], Asset()) - with pytest.raises(TransactionDoesNotExist) as excinfo: + with pytest.raises(TransactionDoesNotExist): 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], 1)]).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_pk, create_tx): @@ -620,7 +641,7 @@ class TestTransactionValidation(object): input_tx = b.get_owned_ids(user_pk).pop() input_transaction = b.get_transaction(input_tx.txid) sk, pk = generate_key_pair() - tx = Transaction.create([pk], [user_pk]) + tx = Transaction.create([pk], [([user_pk], 1)]) tx.operation = 'TRANSFER' tx.asset = input_transaction.asset tx.fulfillments[0].tx_input = input_tx @@ -664,7 +685,8 @@ class TestTransactionValidation(object): input_tx = b.get_owned_ids(user_pk).pop() input_tx = b.get_transaction(input_tx.txid) inputs = input_tx.to_inputs() - transfer_tx = Transaction.transfer(inputs, [user_pk], input_tx.asset) + transfer_tx = Transaction.transfer(inputs, [([user_pk], 1)], + input_tx.asset) transfer_tx = transfer_tx.sign([user_sk]) assert transfer_tx == b.validate_transaction(transfer_tx) @@ -679,16 +701,17 @@ 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_pk, user_sk): + def test_transaction_not_in_valid_block(self, b, user_pk, 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_pk).pop() input_tx = b.get_transaction(input_tx.txid) inputs = input_tx.to_inputs() # create a transaction that's valid but not in a voted valid block - transfer_tx = Transaction.transfer(inputs, [user_pk], input_tx.asset) + transfer_tx = Transaction.transfer(inputs, [([user_pk], 1)], + input_tx.asset) transfer_tx = transfer_tx.sign([user_sk]) assert transfer_tx == b.validate_transaction(transfer_tx) @@ -698,11 +721,12 @@ class TestTransactionValidation(object): b.write_block(block, durability='hard') # create transaction with the undecided input - tx_invalid = Transaction.transfer(transfer_tx.to_inputs(), [user_pk], - transfer_tx.asset) + tx_invalid = Transaction.transfer(transfer_tx.to_inputs(), + [([user_pk], 1)], + transfer_tx.asset) tx_invalid = tx_invalid.sign([user_sk]) - with pytest.raises(FulfillmentNotInValidBlock): + with pytest.raises(TransactionNotInValidBlock): b.validate_transaction(tx_invalid) @@ -797,7 +821,7 @@ class TestMultipleInputs(object): tx_link = b.get_owned_ids(user_pk).pop() input_tx = b.get_transaction(tx_link.txid) inputs = input_tx.to_inputs() - tx = Transaction.transfer(inputs, [user2_pk], input_tx.asset) + tx = Transaction.transfer(inputs, [([user2_pk], 1)], input_tx.asset) tx = tx.sign([user_sk]) # validate transaction @@ -805,69 +829,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_pk): - from bigchaindb.common import crypto - from bigchaindb.models import Transaction - - user2_sk, user2_pk = crypto.generate_key_pair() - - # get inputs - owned_inputs = b.get_owned_ids(user_pk) - 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_pk]]) - 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_pk): - from bigchaindb.common import crypto - from bigchaindb.models import Transaction - - user2_sk, user2_pk = crypto.generate_key_pair() - - # get inputs - owned_inputs = b.get_owned_ids(user_pk) - 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_pk]]) - 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_pk) - 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_pk]) - 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_pk, @@ -881,47 +842,14 @@ class TestMultipleInputs(object): owned_inputs = b.get_owned_ids(user_pk) tx_link = owned_inputs.pop() input_tx = b.get_transaction(tx_link.txid) - tx = Transaction.transfer(input_tx.to_inputs(), [[user2_pk, user3_pk]], input_tx.asset) + tx = Transaction.transfer(input_tx.to_inputs(), + [([user2_pk, user3_pk], 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_pk): - from bigchaindb.common import crypto - from bigchaindb.models import Transaction - - user2_sk, user2_pk = crypto.generate_key_pair() - user3_sk, user3_pk = crypto.generate_key_pair() - - owned_inputs = b.get_owned_ids(user_pk) - 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_pk, user3_pk]]) - 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, @@ -932,7 +860,7 @@ class TestMultipleInputs(object): user2_sk, user2_pk = crypto.generate_key_pair() user3_sk, user3_pk = crypto.generate_key_pair() - tx = Transaction.create([b.me], [user_pk, user2_pk]) + tx = Transaction.create([b.me], [([user_pk, user2_pk], 1)]) tx = tx.sign([b.me_private]) block = b.create_block([tx]) b.write_block(block, durability='hard') @@ -945,7 +873,8 @@ class TestMultipleInputs(object): input_tx = b.get_transaction(owned_input.txid) inputs = input_tx.to_inputs() - transfer_tx = Transaction.transfer(inputs, [user3_pk], input_tx.asset) + transfer_tx = Transaction.transfer(inputs, [([user3_pk], 1)], + input_tx.asset) transfer_tx = transfer_tx.sign([user_sk, user2_sk]) # validate transaction @@ -953,29 +882,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_pk, user2_pk, user2_sk): - from bigchaindb.common import crypto - from bigchaindb.models import Transaction - - # create a new users - user3_sk, user3_pk = crypto.generate_key_pair() - - tx_links = b.get_owned_ids(user_pk) - inputs = sum([b.get_transaction(tx_link.txid).to_inputs() for tx_link - in tx_links], []) - - tx = Transaction.transfer(inputs, len(inputs) * [[user3_pk]]) - 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, @@ -987,7 +893,7 @@ class TestMultipleInputs(object): user3_sk, user3_pk = crypto.generate_key_pair() user4_sk, user4_pk = crypto.generate_key_pair() - tx = Transaction.create([b.me], [user_pk, user2_pk]) + tx = Transaction.create([b.me], [([user_pk, user2_pk], 1)]) tx = tx.sign([b.me_private]) block = b.create_block([tx]) b.write_block(block, durability='hard') @@ -1000,38 +906,14 @@ class TestMultipleInputs(object): tx_link = b.get_owned_ids(user_pk).pop() tx_input = b.get_transaction(tx_link.txid) - tx = Transaction.transfer(tx_input.to_inputs(), [[user3_pk, user4_pk]], tx_input.asset) + tx = Transaction.transfer(tx_input.to_inputs(), + [([user3_pk, user4_pk], 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_pk, - user2_sk, user2_pk): - from bigchaindb.common import crypto - from bigchaindb.models import Transaction - - # create a new users - user3_sk, user3_pk = crypto.generate_key_pair() - user4_sk, user4_pk = crypto.generate_key_pair() - - tx_links = b.get_owned_ids(user_pk) - inputs = sum([b.get_transaction(tx_link.txid).to_inputs() for tx_link - in tx_links], []) - - tx = Transaction.transfer(inputs, len(inputs) * [[user3_pk, user4_pk]]) - 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_pk): from bigchaindb.common import crypto from bigchaindb.common.transaction import TransactionLink @@ -1039,7 +921,7 @@ class TestMultipleInputs(object): user2_sk, user2_pk = crypto.generate_key_pair() - tx = Transaction.create([b.me], [user_pk]) + tx = Transaction.create([b.me], [([user_pk], 1)]) tx = tx.sign([b.me_private]) block = b.create_block([tx]) b.write_block(block, durability='hard') @@ -1049,7 +931,7 @@ class TestMultipleInputs(object): assert owned_inputs_user1 == [TransactionLink(tx.id, 0)] assert owned_inputs_user2 == [] - tx = Transaction.transfer(tx.to_inputs(), [user2_pk], tx.asset) + tx = Transaction.transfer(tx.to_inputs(), [([user2_pk], 1)], tx.asset) tx = tx.sign([user_sk]) block = b.create_block([tx]) b.write_block(block, durability='hard') @@ -1069,7 +951,7 @@ class TestMultipleInputs(object): genesis = b.create_genesis_block() user2_sk, user2_pk = crypto.generate_key_pair() - tx = Transaction.create([b.me], [user_pk]) + tx = Transaction.create([b.me], [([user_pk], 1)]) tx = tx.sign([b.me_private]) block = b.create_block([tx]) b.write_block(block, durability='hard') @@ -1085,7 +967,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_pk], tx.asset) + tx_invalid = Transaction.transfer(tx.to_inputs(), [([user2_pk], 1)], + tx.asset) tx_invalid = tx_invalid.sign([user_sk]) block = b.create_block([tx_invalid]) b.write_block(block, durability='hard') @@ -1101,47 +984,45 @@ 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_pk): - 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_pk = crypto.generate_key_pair() - transactions = [] - for i in range(2): - payload = {'somedata': random.randint(0, 255)} - tx = Transaction.create([b.me], [user_pk], 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_pk], 1), ([user_pk], 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_pk) owned_inputs_user2 = b.get_owned_ids(user2_pk) - 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_pk]]) - 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_pk], 1), ([user2_pk], 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_pk) owned_inputs_user2 = b.get_owned_ids(user2_pk) 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_pk): from bigchaindb.common import crypto @@ -1151,7 +1032,7 @@ class TestMultipleInputs(object): user2_sk, user2_pk = crypto.generate_key_pair() user3_sk, user3_pk = crypto.generate_key_pair() - tx = Transaction.create([b.me], [user_pk, user2_pk]) + tx = Transaction.create([b.me], [([user_pk, user2_pk], 1)]) tx = tx.sign([b.me_private]) block = b.create_block([tx]) b.write_block(block, durability='hard') @@ -1163,7 +1044,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_pk], tx.asset) + tx = Transaction.transfer(tx.to_inputs(), [([user3_pk], 1)], tx.asset) tx = tx.sign([user_sk, user2_sk]) block = b.create_block([tx]) b.write_block(block, durability='hard') @@ -1179,7 +1060,7 @@ class TestMultipleInputs(object): user2_sk, user2_pk = crypto.generate_key_pair() - tx = Transaction.create([b.me], [user_pk]) + tx = Transaction.create([b.me], [([user_pk], 1)]) tx = tx.sign([b.me_private]) block = b.create_block([tx]) b.write_block(block, durability='hard') @@ -1193,7 +1074,7 @@ class TestMultipleInputs(object): assert spent_inputs_user1 is None # create a transaction and block - tx = Transaction.transfer(tx.to_inputs(), [user2_pk], tx.asset) + tx = Transaction.transfer(tx.to_inputs(), [([user2_pk], 1)], tx.asset) tx = tx.sign([user_sk]) block = b.create_block([tx]) b.write_block(block, durability='hard') @@ -1210,7 +1091,7 @@ class TestMultipleInputs(object): # create a new users user2_sk, user2_pk = crypto.generate_key_pair() - tx = Transaction.create([b.me], [user_pk]) + tx = Transaction.create([b.me], [([user_pk], 1)]) tx = tx.sign([b.me_private]) block = b.create_block([tx]) b.write_block(block, durability='hard') @@ -1228,7 +1109,7 @@ class TestMultipleInputs(object): assert spent_inputs_user1 is None # create a transaction and block - tx = Transaction.transfer(tx.to_inputs(), [user2_pk], tx.asset) + tx = Transaction.transfer(tx.to_inputs(), [([user2_pk], 1)], tx.asset) tx = tx.sign([user_sk]) block = b.create_block([tx]) b.write_block(block, durability='hard') @@ -1243,24 +1124,23 @@ 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_pk): - 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_pk = crypto.generate_key_pair() - transactions = [] - for i in range(3): - payload = {'somedata': random.randint(0, 255)} - tx = Transaction.create([b.me], [user_pk], 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_pk], 1), + ([user_pk], 1), + ([user_pk], 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_pk) @@ -1269,22 +1149,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_pk]]) - 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_pk], 1), ([user2_pk], 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_pk): import random @@ -1297,7 +1177,8 @@ class TestMultipleInputs(object): transactions = [] for i in range(3): payload = {'somedata': random.randint(0, 255)} - tx = Transaction.create([b.me], [user_pk, user2_pk], payload) + tx = Transaction.create([b.me], [([user_pk, user2_pk], 1)], + payload) tx = tx.sign([b.me_private]) transactions.append(tx) block = b.create_block(transactions) @@ -1310,7 +1191,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_pk], transactions[0].asset) + tx = Transaction.transfer(transactions[0].to_inputs(), + [([user3_pk], 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 b2c2c9a5..db46a758 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_pk): block_maker = BlockPipeline() for i in range(100): - tx = Transaction.create([b.me], [user_pk]) + tx = Transaction.create([b.me], [([user_pk], 1)]) tx = tx.sign([b.me_private]) block_maker.create(tx) @@ -63,7 +63,7 @@ def test_write_block(b, user_pk): txs = [] for i in range(100): - tx = Transaction.create([b.me], [user_pk]) + tx = Transaction.create([b.me], [([user_pk], 1)]) tx = tx.sign([b.me_private]) txs.append(tx) @@ -82,7 +82,7 @@ def test_duplicate_transaction(b, user_pk): txs = [] for i in range(10): - tx = Transaction.create([b.me], [user_pk]) + tx = Transaction.create([b.me], [([user_pk], 1)]) tx = tx.sign([b.me_private]) txs.append(tx) @@ -109,7 +109,7 @@ def test_delete_tx(b, user_pk): from bigchaindb.pipelines.block import BlockPipeline block_maker = BlockPipeline() for i in range(100): - tx = Transaction.create([b.me], [user_pk]) + tx = Transaction.create([b.me], [([user_pk], 1)]) tx = tx.sign([b.me_private]) block_maker.create(tx) # make sure the tx appears in the backlog @@ -138,7 +138,8 @@ def test_prefeed(b, user_pk): from bigchaindb.pipelines.block import initial for i in range(100): - tx = Transaction.create([b.me], [user_pk], {'msg': random.random()}) + tx = Transaction.create([b.me], [([user_pk], 1)], + {'msg': random.random()}) tx = tx.sign([b.me_private]) b.write_transaction(tx) @@ -167,7 +168,8 @@ def test_full_pipeline(b, user_pk): count_assigned_to_me = 0 for i in range(100): - tx = Transaction.create([b.me], [user_pk], {'msg': random.random()}) + tx = Transaction.create([b.me], [([user_pk], 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 53e7226a..f0d57e1b 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_pk): e = election.Election() # create blocks with transactions - tx1 = Transaction.create([b.me], [user_pk]) + tx1 = Transaction.create([b.me], [([user_pk], 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_pk): e = election.Election() # create blocks with transactions - tx1 = Transaction.create([b.me], [user_pk]) + tx1 = Transaction.create([b.me], [([user_pk], 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_pk): e = election.Election() # create blocks with transactions - tx1 = Transaction.create([b.me], [user_pk]) + tx1 = Transaction.create([b.me], [([user_pk], 1)]) test_block = b.create_block([tx1]) # simulate a federation with four voters @@ -103,7 +103,7 @@ def test_check_requeue_transaction(b, user_pk): e = election.Election() # create blocks with transactions - tx1 = Transaction.create([b.me], [user_pk]) + tx1 = Transaction.create([b.me], [([user_pk], 1)]) test_block = b.create_block([tx1]) e.requeue_transactions(test_block) @@ -131,7 +131,8 @@ def test_full_pipeline(b, user_pk): # write two blocks txs = [] for i in range(100): - tx = Transaction.create([b.me], [user_pk], {'msg': random.random()}) + tx = Transaction.create([b.me], [([user_pk], 1)], + {'msg': random.random()}) tx = tx.sign([b.me_private]) txs.append(tx) @@ -140,7 +141,8 @@ def test_full_pipeline(b, user_pk): txs = [] for i in range(100): - tx = Transaction.create([b.me], [user_pk], {'msg': random.random()}) + tx = Transaction.create([b.me], [([user_pk], 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 1e033382..eb260ffe 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_pk): from bigchaindb.models import Transaction - tx = Transaction.create([b.me], [user_pk]) + tx = Transaction.create([b.me], [([user_pk], 1)]) tx = tx.sign([b.me_private]) b.write_transaction(tx, durability='hard') @@ -27,7 +27,7 @@ def test_get_stale(b, user_pk): def test_reassign_transactions(b, user_pk): from bigchaindb.models import Transaction # test with single node - tx = Transaction.create([b.me], [user_pk]) + tx = Transaction.create([b.me], [([user_pk], 1)]) tx = tx.sign([b.me_private]) b.write_transaction(tx, durability='hard') @@ -36,7 +36,7 @@ def test_reassign_transactions(b, user_pk): stm.reassign_transactions(tx.to_dict()) # test with federation - tx = Transaction.create([b.me], [user_pk]) + tx = Transaction.create([b.me], [([user_pk], 1)]) tx = tx.sign([b.me_private]) b.write_transaction(tx, durability='hard') @@ -51,7 +51,7 @@ def test_reassign_transactions(b, user_pk): assert reassigned_tx['assignee'] != tx['assignee'] # test with node not in federation - tx = Transaction.create([b.me], [user_pk]) + tx = Transaction.create([b.me], [([user_pk], 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_pk): monkeypatch.setattr('time.time', lambda: 1) for i in range(100): - tx = Transaction.create([b.me], [user_pk]) + tx = Transaction.create([b.me], [([user_pk], 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 0810db18..f61bf253 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_pk): 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_pk): 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_pk): 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/test_models.py b/tests/test_models.py index afde33fb..f462e3fe 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() diff --git a/tests/web/conftest.py b/tests/web/conftest.py index f6d9f9c6..988b0912 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 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_basic_views.py b/tests/web/test_transactions.py similarity index 86% rename from tests/web/test_basic_views.py rename to tests/web/test_transactions.py index 9fa75758..2bc27d0c 100644 --- a/tests/web/test_basic_views.py +++ b/tests/web/test_transactions.py @@ -25,18 +25,11 @@ 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() - 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 +41,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 +53,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 +70,8 @@ def test_post_transfer_transaction_endpoint(b, client, user_pk, user_sk): input_valid = b.get_owned_ids(user_pk).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 +88,8 @@ def test_post_invalid_transfer_transaction_returns_400(b, client, user_pk, user_ input_valid = b.get_owned_ids(user_pk).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