diff --git a/.travis.yml b/.travis.yml index a315789c..bd8ad0b1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,7 +16,9 @@ install: - pip install -e .[test] - pip install codecov -before_script: rethinkdb --daemon +before_script: + - flake8 --max-line-length 119 bigchaindb/ + - rethinkdb --daemon script: py.test -n auto -s -v --cov=bigchaindb 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/bigchaindb/__init__.py b/bigchaindb/__init__.py index 1276f9a5..dc31b148 100644 --- a/bigchaindb/__init__.py +++ b/bigchaindb/__init__.py @@ -11,8 +11,8 @@ config = { # Note: this section supports all the Gunicorn settings: # - http://docs.gunicorn.org/en/stable/settings.html 'bind': os.environ.get('BIGCHAINDB_SERVER_BIND') or 'localhost:9984', - 'workers': None, # if none, the value will be cpu_count * 2 + 1 - 'threads': None, # if none, the value will be cpu_count * 2 + 1 + 'workers': None, # if none, the value will be cpu_count * 2 + 1 + 'threads': None, # if none, the value will be cpu_count * 2 + 1 }, 'database': { 'host': os.environ.get('BIGCHAINDB_DATABASE_HOST', 'localhost'), diff --git a/bigchaindb/commands/bigchain.py b/bigchaindb/commands/bigchain.py index 40891416..d52131ab 100644 --- a/bigchaindb/commands/bigchain.py +++ b/bigchaindb/commands/bigchain.py @@ -82,7 +82,6 @@ def run_configure(args, skip_if_exists=False): conf, bigchaindb.config_utils.env_config(bigchaindb.config)) - print('Generating keypair', file=sys.stderr) conf['keypair']['private'], conf['keypair']['public'] = \ crypto.generate_key_pair() @@ -162,7 +161,7 @@ def run_start(args): if args.allow_temp_keypair: if not (bigchaindb.config['keypair']['private'] or - bigchaindb.config['keypair']['public']): + bigchaindb.config['keypair']['public']): private_key, public_key = crypto.generate_key_pair() bigchaindb.config['keypair']['private'] = private_key @@ -170,7 +169,6 @@ def run_start(args): else: logger.warning('Keypair found, no need to create one on the fly.') - if args.start_rethinkdb: try: proc = utils.start_rethinkdb() diff --git a/bigchaindb/commands/utils.py b/bigchaindb/commands/utils.py index 6619da6d..573ba785 100644 --- a/bigchaindb/commands/utils.py +++ b/bigchaindb/commands/utils.py @@ -14,7 +14,6 @@ from bigchaindb import db from bigchaindb.version import __version__ - def start_rethinkdb(): """Start RethinkDB as a child process and wait for it to be available. diff --git a/bigchaindb/common/transaction.py b/bigchaindb/common/transaction.py index 41d8a30c..c87b9864 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 SigningKey, hash_data from bigchaindb.common.exceptions import (KeypairMismatchException, - InvalidHash, InvalidSignature) + InvalidHash, InvalidSignature, + AmountError, AssetIdMismatch) from bigchaindb.common.util import serialize, gen_timestamp @@ -38,17 +38,15 @@ class Fulfillment(object): TransactionLink`, optional): A link representing the input of a `TRANSFER` Transaction. """ - self.fulfillment = fulfillment - if tx_input is not None and not isinstance(tx_input, TransactionLink): raise TypeError('`tx_input` must be a TransactionLink instance') - else: - self.tx_input = tx_input if not isinstance(owners_before, list): raise TypeError('`owners_after` must be a list instance') - else: - self.owners_before = owners_before + + self.fulfillment = fulfillment + self.tx_input = tx_input + self.owners_before = owners_before def __eq__(self, other): # TODO: If `other !== Fulfillment` return `False` @@ -98,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. @@ -216,14 +222,13 @@ class Condition(object): Raises: TypeError: if `owners_after` is not instance of `list`. """ + if not isinstance(owners_after, list) and owners_after is not None: + raise TypeError('`owners_after` must be a list instance or None') + self.fulfillment = fulfillment # TODO: Not sure if we should validate for value here self.amount = amount - - if not isinstance(owners_after, list) and owners_after is not None: - raise TypeError('`owners_after` must be a list instance or None') - else: - self.owners_after = owners_after + self.owners_after = owners_after def __eq__(self, other): # TODO: If `other !== Condition` return `False` @@ -268,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: @@ -278,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: @@ -316,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): @@ -341,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) @@ -423,7 +415,7 @@ class Asset(object): self.updatable = updatable self.refillable = refillable - self._validate_asset() + self.validate_asset() def __eq__(self, other): try: @@ -466,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') @@ -477,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() == self.to_dict() + + @classmethod + def from_dict(cls, link): + """Transforms a Python dictionary to a AssetLink object. + + Args: + link (dict): The link to be transformed. + + Returns: + :class:`~bigchaindb.common.transaction.AssetLink` + """ + try: + return cls(link['id']) + except TypeError: + return cls() + + def to_dict(self): + """Transforms the object to a Python dictionary. + + Returns: + (dict|None): The link as an alternative serialization format. + """ + if self.data_id is None: + return None + else: + return { + 'id': self.data_id + } + class Metadata(object): """Metadata is used to store a dictionary and its hash in a Transaction.""" @@ -493,16 +587,12 @@ class Metadata(object): data_id (str): A hash corresponding to the contents of `data`. """ - # TODO: Rename `payload_id` to `id` - if data_id is not None: - self.data_id = data_id - else: - self.data_id = self.to_hash() - if data is not None and not isinstance(data, dict): raise TypeError('`data` must be a dict instance or None') - else: - self.data = data + + # TODO: Rename `payload_id` to `id` + self.data_id = data_id if data_id is not None else self.to_hash() + self.data = data def __eq__(self, other): # TODO: If `other !== Data` return `False` @@ -592,55 +682,44 @@ class Transaction(object): version (int): Defines the version number of a Transaction. """ - if version is not None: - self.version = version - else: - self.version = self.__class__.VERSION - - if timestamp is not None: - self.timestamp = timestamp - else: - self.timestamp = gen_timestamp() - if operation not in Transaction.ALLOWED_OPERATIONS: allowed_ops = ', '.join(self.__class__.ALLOWED_OPERATIONS) raise ValueError('`operation` must be one of {}' .format(allowed_ops)) - else: - self.operation = operation - # If an asset is not defined in a `CREATE` transaction, create a - # default one. - if asset is None and operation == Transaction.CREATE: - asset = Asset() - - if not isinstance(asset, Asset): + # Only assets for 'CREATE' operations can be un-defined. + if (asset and not isinstance(asset, Asset) or + not asset and operation != Transaction.CREATE): raise TypeError('`asset` must be an Asset instance') - else: - self.asset = asset - if conditions is not None and not isinstance(conditions, list): + if conditions and not isinstance(conditions, list): raise TypeError('`conditions` must be a list instance or None') - elif conditions is None: - self.conditions = [] - else: - self.conditions = conditions - if fulfillments is not None and not isinstance(fulfillments, list): + if fulfillments and not isinstance(fulfillments, list): raise TypeError('`fulfillments` must be a list instance or None') - elif fulfillments is None: - self.fulfillments = [] - else: - self.fulfillments = fulfillments if metadata is not None and not isinstance(metadata, Metadata): raise TypeError('`metadata` must be a Metadata instance or None') - else: - self.metadata = metadata + + self.version = version if version is not None else self.VERSION + self.timestamp = timestamp if timestamp else gen_timestamp() + self.operation = operation + self.asset = asset if asset else Asset() + self.conditions = conditions if conditions else [] + 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: @@ -648,7 +727,6 @@ class Transaction(object): use cases: - Ed25519 - ThresholdSha256 - - PreimageSha256. Additionally, it provides support for the following BigchainDB use cases: @@ -663,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` @@ -675,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): @@ -769,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) @@ -812,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. @@ -894,13 +936,12 @@ class Transaction(object): key_pairs = {gen_public_key(SigningKey(private_key)): SigningKey(private_key) for private_key in private_keys} - zippedIO = enumerate(zip(self.fulfillments, self.conditions)) - for index, (fulfillment, condition) in zippedIO: + 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() @@ -1057,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) @@ -1075,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, @@ -1240,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/config_utils.py b/bigchaindb/config_utils.py index 3b571835..e678f4e9 100644 --- a/bigchaindb/config_utils.py +++ b/bigchaindb/config_utils.py @@ -6,7 +6,7 @@ determined according to the following rules: * If it's set by an environment variable, then use that value * Otherwise, if it's set in a local config file, then use that value -* Otherwise, use the default value (contained in +* Otherwise, use the default value (contained in ``bigchaindb.__init__``) """ diff --git a/bigchaindb/core.py b/bigchaindb/core.py index a5ec25e0..8045e6ba 100644 --- a/bigchaindb/core.py +++ b/bigchaindb/core.py @@ -6,13 +6,11 @@ from time import time from itertools import compress from bigchaindb.common import crypto, exceptions from bigchaindb.common.util import gen_timestamp, serialize -from bigchaindb.common.transaction import TransactionLink, Metadata - -import rethinkdb as r +from bigchaindb.common.transaction import TransactionLink, Asset import bigchaindb -from bigchaindb.db.utils import Connection +from bigchaindb.db.utils import Connection, get_backend from bigchaindb import config_utils, util from bigchaindb.consensus import BaseConsensusRules from bigchaindb.models import Block, Transaction @@ -33,7 +31,7 @@ class Bigchain(object): # return if transaction is in backlog TX_IN_BACKLOG = 'backlog' - def __init__(self, host=None, port=None, dbname=None, + def __init__(self, host=None, port=None, dbname=None, backend=None, public_key=None, private_key=None, keyring=[], backlog_reassign_delay=None): """Initialize the Bigchain instance @@ -51,6 +49,8 @@ class Bigchain(object): host (str): hostname where RethinkDB is running. port (int): port in which RethinkDB is running (usually 28015). dbname (str): the name of the database to connect to (usually bigchain). + backend (:class:`~bigchaindb.db.backends.rethinkdb.RehinkDBBackend`): + the database backend to use. public_key (str): the base58 encoded public key for the ED25519 curve. private_key (str): the base58 encoded private key for the ED25519 curve. keyring (list[str]): list of base58 encoded public keys of the federation nodes. @@ -60,6 +60,7 @@ class Bigchain(object): self.host = host or bigchaindb.config['database']['host'] self.port = port or bigchaindb.config['database']['port'] self.dbname = dbname or bigchaindb.config['database']['name'] + self.backend = backend or get_backend(host, port, dbname) self.me = public_key or bigchaindb.config['keypair']['public'] self.me_private = private_key or bigchaindb.config['keypair']['private'] self.nodes_except_me = keyring or bigchaindb.config['keyring'] @@ -73,9 +74,6 @@ class Bigchain(object): self.connection = Connection(host=self.host, port=self.port, db=self.dbname) - def reconnect(self): - return r.connect(host=self.host, port=self.port, db=self.dbname) - def write_transaction(self, signed_transaction, durability='soft'): """Write the transaction to bigchain. @@ -102,12 +100,9 @@ class Bigchain(object): signed_transaction.update({'assignment_timestamp': time()}) # write to the backlog - response = self.connection.run( - r.table('backlog') - .insert(signed_transaction, durability=durability)) - return response + return self.backend.write_transaction(signed_transaction) - def reassign_transaction(self, transaction, durability='hard'): + def reassign_transaction(self, transaction): """Assign a transaction to a new node Args: @@ -131,23 +126,30 @@ class Bigchain(object): # There is no other node to assign to new_assignee = self.me - response = self.connection.run( - r.table('backlog') - .get(transaction['id']) - .update({'assignee': new_assignee, 'assignment_timestamp': time()}, - durability=durability)) - return response + return self.backend.update_transaction( + transaction['id'], + {'assignee': new_assignee, 'assignment_timestamp': time()}) + + def delete_transaction(self, *transaction_id): + """Delete a transaction from the backlog. + + Args: + *transaction_id (str): the transaction(s) to delete + + Returns: + The database response. + """ + + return self.backend.delete_transaction(*transaction_id) def get_stale_transactions(self): - """Get a RethinkDB cursor of stale transactions + """Get a cursor of stale transactions. Transactions are considered stale if they have been assigned a node, but are still in the backlog after some amount of time specified in the configuration """ - return self.connection.run( - r.table('backlog') - .filter(lambda tx: time() - tx['assignment_timestamp'] > self.backlog_reassign_delay)) + return self.backend.get_stale_transactions(self.backlog_reassign_delay) def validate_transaction(self, transaction): """Validate a transaction. @@ -180,26 +182,55 @@ 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.TransactionNotInValidBlock): + exceptions.TransactionNotInValidBlock, exceptions.AmountError): return False - def get_transaction(self, txid, include_status=False): - """Retrieve a transaction with `txid` from bigchain. + def get_block(self, block_id, include_status=False): + """Get the block with the specified `block_id` (and optionally its status) - Queries the bigchain for a transaction, if it's in a valid or invalid - block. + Returns the block corresponding to `block_id` or None if no match is + found. Args: - txid (str): transaction id of the transaction to query + 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) + + This query begins by looking in the bigchain table for all blocks containing + a transaction with the specified `txid`. If one of those blocks is valid, it + returns the matching transaction from that block. Else if some of those + blocks are undecided, it returns a matching transaction from one of them. If + the transaction was found in invalid blocks only, or in no blocks, then this + query looks for a matching transaction in the backlog table, and if it finds + one there, it returns that. + + Args: + txid (str): transaction id of the transaction to get include_status (bool): also return the status of the transaction the return value is then a tuple: (tx, status) Returns: A :class:`~.models.Transaction` instance if the transaction - was found, otherwise ``None``. + was found in a valid block, an undecided block, or the backlog table, + otherwise ``None``. If :attr:`include_status` is ``True``, also returns the transaction's status if the transaction was found. """ @@ -207,36 +238,33 @@ class Bigchain(object): response, tx_status = None, None validity = self.get_blocks_status_containing_tx(txid) + check_backlog = True if validity: # Disregard invalid blocks, and return if there are no valid or undecided blocks validity = {_id: status for _id, status in validity.items() - if status != Bigchain.BLOCK_INVALID} + if status != Bigchain.BLOCK_INVALID} if validity: + # The transaction _was_ found in an undecided or valid block, + # so there's no need to look in the backlog table + check_backlog = False + tx_status = self.TX_UNDECIDED # If the transaction is in a valid or any undecided block, return it. Does not check - # if transactions in undecided blocks are consistent, but selects the valid block before - # undecided ones + # if transactions in undecided blocks are consistent, but selects the valid block + # before undecided ones for target_block_id in validity: if validity[target_block_id] == Bigchain.BLOCK_VALID: tx_status = self.TX_VALID break # Query the transaction in the target block and return - response = self.connection.run( - r.table('bigchain', read_mode=self.read_mode) - .get(target_block_id) - .get_field('block') - .get_field('transactions') - .filter(lambda tx: tx['id'] == txid))[0] + response = self.backend.get_transaction_from_block(txid, target_block_id) + + if check_backlog: + response = self.backend.get_transaction_from_backlog(txid) - else: - # Otherwise, check the backlog - response = self.connection.run(r.table('backlog') - .get(txid) - .without('assignee', 'assignment_timestamp') - .default(None)) if response: tx_status = self.TX_IN_BACKLOG @@ -262,24 +290,6 @@ class Bigchain(object): _, status = self.get_transaction(txid, include_status=True) return status - def search_block_election_on_index(self, value, index): - """Retrieve block election information given a secondary index and value - - Args: - value: a value to search (e.g. transaction id string, payload hash string) - index (str): name of a secondary index, e.g. 'transaction_id' - - Returns: - :obj:`list` of :obj:`dict`: A list of blocks with with only election information - """ - # First, get information on all blocks which contain this transaction - response = self.connection.run( - r.table('bigchain', read_mode=self.read_mode) - .get_all(value, index=index) - .pluck('votes', 'id', {'block': ['voters']})) - - return list(response) - def get_blocks_status_containing_tx(self, txid): """Retrieve block ids and statuses related to a transaction @@ -294,7 +304,7 @@ class Bigchain(object): """ # First, get information on all blocks which contain this transaction - blocks = self.search_block_election_on_index(txid, 'transaction_id') + blocks = self.backend.get_blocks_status_from_transaction(txid) if blocks: # Determine the election status of each block validity = { @@ -305,7 +315,7 @@ class Bigchain(object): } # NOTE: If there are multiple valid blocks with this transaction, - # something has gone wrong + # something has gone wrong if list(validity.values()).count(Bigchain.BLOCK_VALID) > 1: block_ids = str([block for block in validity if validity[block] == Bigchain.BLOCK_VALID]) @@ -336,14 +346,8 @@ class Bigchain(object): A list of transactions containing that metadata. If no transaction exists with that metadata it returns an empty list `[]` """ - cursor = self.connection.run( - r.table('bigchain', read_mode=self.read_mode) - .get_all(metadata_id, index='metadata_id') - .concat_map(lambda block: block['block']['transactions']) - .filter(lambda transaction: transaction['transaction']['metadata']['id'] == metadata_id)) - - transactions = list(cursor) - return [Transaction.from_dict(tx) for tx in transactions] + cursor = self.backend.get_transactions_by_metadata_id(metadata_id) + return [Transaction.from_dict(tx) for tx in cursor] def get_txs_by_asset_id(self, asset_id): """Retrieves transactions related to a particular asset. @@ -358,14 +362,25 @@ class Bigchain(object): A list of transactions containing related to the asset. If no transaction exists for that asset it returns an empty list `[]` """ - cursor = self.connection.run( - r.table('bigchain', read_mode=self.read_mode) - .get_all(asset_id, index='asset_id') - .concat_map(lambda block: block['block']['transactions']) - .filter(lambda transaction: transaction['transaction']['asset']['id'] == asset_id)) + 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. @@ -382,13 +397,7 @@ class Bigchain(object): """ # checks if an input was already spent # checks if the bigchain has any transaction with input {'txid': ..., 'cid': ...} - response = self.connection.run( - r.table('bigchain', read_mode=self.read_mode) - .concat_map(lambda doc: doc['block']['transactions']) - .filter(lambda transaction: transaction['transaction']['fulfillments'] - .contains(lambda fulfillment: fulfillment['input'] == {'txid': txid, 'cid': cid}))) - - transactions = list(response) + transactions = list(self.backend.get_spent(txid, cid)) # a transaction_id should have been spent at most one time if transactions: @@ -400,8 +409,9 @@ class Bigchain(object): if self.get_transaction(transaction['id']): num_valid_transactions += 1 if num_valid_transactions > 1: - raise exceptions.DoubleSpend('`{}` was spent more then once. There is a problem with the chain'.format( - txid)) + raise exceptions.DoubleSpend( + '`{}` was spent more then once. There is a problem with the chain'.format( + txid)) if num_valid_transactions: return Transaction.from_dict(transactions[0]) @@ -423,12 +433,7 @@ class Bigchain(object): """ # get all transactions in which owner is in the `owners_after` list - response = self.connection.run( - r.table('bigchain', read_mode=self.read_mode) - .concat_map(lambda doc: doc['block']['transactions']) - .filter(lambda tx: tx['transaction']['conditions'] - .contains(lambda c: c['owners_after'] - .contains(owner)))) + response = self.backend.get_owned_ids(owner) owned = [] for tx in response: @@ -439,7 +444,7 @@ class Bigchain(object): continue # NOTE: It's OK to not serialize the transaction here, as we do not - # use it after the execution of this function. + # use it after the execution of this function. # a transaction can contain multiple outputs (conditions) so we need to iterate over all of them # to get a list of outputs available to spend for index, cond in enumerate(tx['transaction']['conditions']): @@ -513,9 +518,7 @@ class Bigchain(object): but the vote is invalid. """ - votes = list(self.connection.run( - r.table('votes', read_mode=self.read_mode) - .get_all([block_id, self.me], index='block_and_voter'))) + votes = list(self.backend.get_votes_by_block_id_and_voter(block_id, self.me)) if len(votes) > 1: raise exceptions.MultipleVotesError('Block {block_id} has {n_votes} votes from public key {me}' @@ -537,21 +540,16 @@ class Bigchain(object): block (Block): block to write to bigchain. """ - self.connection.run( - r.table('bigchain') - .insert(r.json(block.to_str()), durability=durability)) + return self.backend.write_block(block.to_str(), durability=durability) def transaction_exists(self, transaction_id): - response = self.connection.run( - r.table('bigchain', read_mode=self.read_mode)\ - .get_all(transaction_id, index='transaction_id')) - return len(response.items) > 0 + return self.backend.has_transaction(transaction_id) def prepare_genesis_block(self): """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 @@ -574,9 +572,7 @@ class Bigchain(object): # 2. create the block with one transaction # 3. write the block to the bigchain - blocks_count = self.connection.run( - r.table('bigchain', read_mode=self.read_mode) - .count()) + blocks_count = self.backend.count_blocks() if blocks_count: raise exceptions.GenesisBlockAlreadyExistsError('Cannot create the Genesis block') @@ -588,7 +584,7 @@ class Bigchain(object): def vote(self, block_id, previous_block_id, decision, invalid_reason=None): """Create a signed vote for a block given the - :attr:`previous_block_id` and the :attr:`decision` (valid/invalid). + :attr:`previous_block_id` and the :attr:`decision` (valid/invalid). Args: block_id (str): The id of the block to vote on. @@ -621,69 +617,12 @@ class Bigchain(object): def write_vote(self, vote): """Write the vote to the database.""" - - self.connection.run( - r.table('votes') - .insert(vote)) + return self.backend.write_vote(vote) def get_last_voted_block(self): """Returns the last block that this node voted on.""" - try: - # get the latest value for the vote timestamp (over all votes) - max_timestamp = self.connection.run( - r.table('votes', read_mode=self.read_mode) - .filter(r.row['node_pubkey'] == self.me) - .max(r.row['vote']['timestamp']))['vote']['timestamp'] - - last_voted = list(self.connection.run( - r.table('votes', read_mode=self.read_mode) - .filter(r.row['vote']['timestamp'] == max_timestamp) - .filter(r.row['node_pubkey'] == self.me))) - - except r.ReqlNonExistenceError: - # return last vote if last vote exists else return Genesis block - res = self.connection.run( - r.table('bigchain', read_mode=self.read_mode) - .filter(util.is_genesis_block)) - block = list(res)[0] - return Block.from_dict(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 - # then we need to rebuild the chain for the blocks that have been retrieved - # to get the last one. - - # Given a block_id, mapping returns the id of the block pointing at it. - mapping = {v['vote']['previous_block']: v['vote']['voting_for_block'] - for v in last_voted} - - # Since we follow the chain backwards, we can start from a random - # point of the chain and "move up" from it. - last_block_id = list(mapping.values())[0] - - # We must be sure to break the infinite loop. This happens when: - # - the block we are currenty iterating is the one we are looking for. - # This will trigger a KeyError, breaking the loop - # - we are visiting again a node we already explored, hence there is - # a loop. This might happen if a vote points both `previous_block` - # and `voting_for_block` to the same `block_id` - explored = set() - - while True: - try: - if last_block_id in explored: - raise exceptions.CyclicBlockchainError() - explored.add(last_block_id) - last_block_id = mapping[last_block_id] - except KeyError: - break - - res = self.connection.run( - r.table('bigchain', read_mode=self.read_mode) - .get(last_block_id)) - - return Block.from_dict(res) + return Block.from_dict(self.backend.get_last_voted_block(self.me)) def get_unvoted_blocks(self): """Return all the blocks that have not been voted on by this node. @@ -692,37 +631,26 @@ class Bigchain(object): :obj:`list` of :obj:`dict`: a list of unvoted blocks """ - unvoted = self.connection.run( - r.table('bigchain', read_mode=self.read_mode) - .filter(lambda block: r.table('votes', read_mode=self.read_mode) - .get_all([block['id'], self.me], index='block_and_voter') - .is_empty()) - .order_by(r.asc(r.row['block']['timestamp']))) - - # FIXME: I (@vrde) don't like this solution. Filtering should be done at a - # database level. Solving issue #444 can help untangling the situation - unvoted_blocks = filter(lambda block: not util.is_genesis_block(block), unvoted) - return unvoted_blocks + # XXX: should this return instaces of Block? + return self.backend.get_unvoted_blocks(self.me) def block_election_status(self, block_id, voters): """Tally the votes on a block, and return the status: valid, invalid, or undecided.""" - votes = self.connection.run(r.table('votes', read_mode=self.read_mode) - .between([block_id, r.minval], [block_id, r.maxval], index='block_and_voter')) - - votes = list(votes) - + votes = list(self.backend.get_votes_by_block_id(block_id)) n_voters = len(voters) voter_counts = collections.Counter([vote['node_pubkey'] for vote in votes]) for node in voter_counts: if voter_counts[node] > 1: - raise exceptions.MultipleVotesError('Block {block_id} has multiple votes ({n_votes}) from voting node {node_id}' - .format(block_id=block_id, n_votes=str(voter_counts[node]), node_id=node)) + raise exceptions.MultipleVotesError( + 'Block {block_id} has multiple votes ({n_votes}) from voting node {node_id}' + .format(block_id=block_id, n_votes=str(voter_counts[node]), node_id=node)) if len(votes) > n_voters: raise exceptions.MultipleVotesError('Block {block_id} has {n_votes} votes cast, but only {n_voters} voters' - .format(block_id=block_id, n_votes=str(len(votes)), n_voters=str(n_voters))) + .format(block_id=block_id, n_votes=str(len(votes)), + n_voters=str(n_voters))) # vote_cast is the list of votes e.g. [True, True, False] vote_cast = [vote['vote']['is_block_valid'] for vote in votes] diff --git a/bigchaindb/db/__init__.py b/bigchaindb/db/__init__.py index 9c299f48..28ebfc3a 100644 --- a/bigchaindb/db/__init__.py +++ b/bigchaindb/db/__init__.py @@ -1,2 +1,2 @@ # TODO can we use explicit imports? -from bigchaindb.db.utils import * +from bigchaindb.db.utils import * # noqa: F401,F403 diff --git a/bigchaindb/db/backends/__init__.py b/bigchaindb/db/backends/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/bigchaindb/db/backends/rethinkdb.py b/bigchaindb/db/backends/rethinkdb.py new file mode 100644 index 00000000..b9355b41 --- /dev/null +++ b/bigchaindb/db/backends/rethinkdb.py @@ -0,0 +1,431 @@ +"""Backend implementation for RethinkDB. + +This module contains all the methods to store and retrieve data from RethinkDB. +""" + +from time import time + +import rethinkdb as r + +from bigchaindb import util +from bigchaindb.db.utils import Connection +from bigchaindb.common import exceptions + + +class RethinkDBBackend: + + def __init__(self, host=None, port=None, db=None): + """Initialize a new RethinkDB Backend instance. + + Args: + host (str): the host to connect to. + port (int): the port to connect to. + db (str): the name of the database to use. + """ + + self.read_mode = 'majority' + self.durability = 'soft' + self.connection = Connection(host=host, port=port, db=db) + + def write_transaction(self, signed_transaction): + """Write a transaction to the backlog table. + + Args: + signed_transaction (dict): a signed transaction. + + Returns: + The result of the operation. + """ + + return self.connection.run( + r.table('backlog') + .insert(signed_transaction, durability=self.durability)) + + def update_transaction(self, transaction_id, doc): + """Update a transaction in the backlog table. + + Args: + transaction_id (str): the id of the transaction. + doc (dict): the values to update. + + Returns: + The result of the operation. + """ + + return self.connection.run( + r.table('backlog') + .get(transaction_id) + .update(doc)) + + def delete_transaction(self, *transaction_id): + """Delete a transaction from the backlog. + + Args: + *transaction_id (str): the transaction(s) to delete + + Returns: + The database response. + """ + + return self.connection.run( + r.table('backlog') + .get_all(*transaction_id) + .delete(durability='hard')) + + def get_stale_transactions(self, reassign_delay): + """Get a cursor of stale transactions. + + Transactions are considered stale if they have been assigned a node, + but are still in the backlog after some amount of time specified in the + configuration. + + Args: + reassign_delay (int): threshold (in seconds) to mark a transaction stale. + + Returns: + A cursor of transactions. + """ + + return self.connection.run( + r.table('backlog') + .filter(lambda tx: time() - tx['assignment_timestamp'] > reassign_delay)) + + def get_transaction_from_block(self, transaction_id, block_id): + """Get a transaction from a specific block. + + Args: + transaction_id (str): the id of the transaction. + block_id (str): the id of the block. + + Returns: + The matching transaction. + """ + return self.connection.run( + r.table('bigchain', read_mode=self.read_mode) + .get(block_id) + .get_field('block') + .get_field('transactions') + .filter(lambda tx: tx['id'] == transaction_id))[0] + + def get_transaction_from_backlog(self, transaction_id): + """Get a transaction from backlog. + + Args: + transaction_id (str): the id of the transaction. + + Returns: + The matching transaction. + """ + return self.connection.run( + r.table('backlog') + .get(transaction_id) + .without('assignee', 'assignment_timestamp') + .default(None)) + + def get_blocks_status_from_transaction(self, transaction_id): + """Retrieve block election information given a secondary index and value + + Args: + value: a value to search (e.g. transaction id string, payload hash string) + index (str): name of a secondary index, e.g. 'transaction_id' + + Returns: + :obj:`list` of :obj:`dict`: A list of blocks with with only election information + """ + + return self.connection.run( + r.table('bigchain', read_mode=self.read_mode) + .get_all(transaction_id, index='transaction_id') + .pluck('votes', 'id', {'block': ['voters']})) + + def get_transactions_by_metadata_id(self, metadata_id): + """Retrieves transactions related to a metadata. + + When creating a transaction one of the optional arguments is the `metadata`. The metadata is a generic + dict that contains extra information that can be appended to the transaction. + + To make it easy to query the bigchain for that particular metadata we create a UUID for the metadata and + store it with the transaction. + + Args: + metadata_id (str): the id for this particular metadata. + + Returns: + A list of transactions containing that metadata. If no transaction exists with that metadata it + returns an empty list `[]` + """ + return self.connection.run( + r.table('bigchain', read_mode=self.read_mode) + .get_all(metadata_id, index='metadata_id') + .concat_map(lambda block: block['block']['transactions']) + .filter(lambda transaction: transaction['transaction']['metadata']['id'] == metadata_id)) + + def get_transactions_by_asset_id(self, asset_id): + """Retrieves transactions related to a particular asset. + + A digital asset in bigchaindb is identified by an uuid. This allows us to query all the transactions + related to a particular digital asset, knowing the id. + + Args: + asset_id (str): the id for this particular metadata. + + Returns: + A list of transactions containing related to the asset. If no transaction exists for that asset it + returns an empty list `[]` + """ + + 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)) + + 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. + + A transaction can be used as an input for another transaction. Bigchain needs to make sure that a + given `txid` is only used once. + + Args: + transaction_id (str): The id of the transaction. + condition_id (int): The index of the condition in the respective transaction. + + Returns: + The transaction that used the `txid` as an input else `None` + """ + + # TODO: use index! + return self.connection.run( + r.table('bigchain', read_mode=self.read_mode) + .concat_map(lambda doc: doc['block']['transactions']) + .filter(lambda transaction: transaction['transaction']['fulfillments'].contains( + lambda fulfillment: fulfillment['input'] == {'txid': transaction_id, 'cid': condition_id}))) + + def get_owned_ids(self, owner): + """Retrieve a list of `txids` that can we used has inputs. + + Args: + owner (str): base58 encoded public key. + + Returns: + A cursor for the matching transactions. + """ + + # TODO: use index! + return self.connection.run( + r.table('bigchain', read_mode=self.read_mode) + .concat_map(lambda doc: doc['block']['transactions']) + .filter(lambda tx: tx['transaction']['conditions'].contains( + lambda c: c['owners_after'].contains(owner)))) + + def get_votes_by_block_id(self, block_id): + """Get all the votes casted for a specific block. + + Args: + block_id (str): the block id to use. + + Returns: + A cursor for the matching votes. + """ + return self.connection.run( + r.table('votes', read_mode=self.read_mode) + .between([block_id, r.minval], [block_id, r.maxval], index='block_and_voter')) + + def get_votes_by_block_id_and_voter(self, block_id, node_pubkey): + """Get all the votes casted for a specific block by a specific voter. + + Args: + block_id (str): the block id to use. + node_pubkey (str): base58 encoded public key + + Returns: + A cursor for the matching votes. + """ + return self.connection.run( + r.table('votes', read_mode=self.read_mode) + .get_all([block_id, node_pubkey], index='block_and_voter')) + + def write_block(self, block, durability='soft'): + """Write a block to the bigchain table. + + Args: + block (dict): the block to write. + + Returns: + The database response. + """ + return self.connection.run( + 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. + + Args: + transaction_id (str): the id of the transaction to check. + + Returns: + ``True`` if the transaction exists, ``False`` otherwise. + """ + return bool(self.connection.run( + r.table('bigchain', read_mode=self.read_mode) + .get_all(transaction_id, index='transaction_id').count())) + + def count_blocks(self): + """Count the number of blocks in the bigchain table. + + Returns: + The number of blocks. + """ + + return self.connection.run( + 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. + + Args: + vote (dict): the vote to write. + + Returns: + The database response. + """ + return self.connection.run( + 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. + + Args: + node_pubkey (str): base58 encoded public key. + + Returns: + The last block the node has voted on. If the node didn't cast + any vote then the genesis block is returned. + """ + try: + # get the latest value for the vote timestamp (over all votes) + max_timestamp = self.connection.run( + r.table('votes', read_mode=self.read_mode) + .filter(r.row['node_pubkey'] == node_pubkey) + .max(r.row['vote']['timestamp']))['vote']['timestamp'] + + last_voted = list(self.connection.run( + r.table('votes', read_mode=self.read_mode) + .filter(r.row['vote']['timestamp'] == max_timestamp) + .filter(r.row['node_pubkey'] == node_pubkey))) + + except r.ReqlNonExistenceError: + # return last vote if last vote exists else return Genesis block + 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 + # then we need to rebuild the chain for the blocks that have been retrieved + # to get the last one. + + # Given a block_id, mapping returns the id of the block pointing at it. + mapping = {v['vote']['previous_block']: v['vote']['voting_for_block'] + for v in last_voted} + + # Since we follow the chain backwards, we can start from a random + # point of the chain and "move up" from it. + last_block_id = list(mapping.values())[0] + + # We must be sure to break the infinite loop. This happens when: + # - the block we are currenty iterating is the one we are looking for. + # This will trigger a KeyError, breaking the loop + # - we are visiting again a node we already explored, hence there is + # a loop. This might happen if a vote points both `previous_block` + # and `voting_for_block` to the same `block_id` + explored = set() + + while True: + try: + if last_block_id in explored: + raise exceptions.CyclicBlockchainError() + explored.add(last_block_id) + last_block_id = mapping[last_block_id] + except KeyError: + break + + return self.connection.run( + r.table('bigchain', read_mode=self.read_mode) + .get(last_block_id)) + + def get_unvoted_blocks(self, node_pubkey): + """Return all the blocks that have not been voted by the specified node. + + Args: + node_pubkey (str): base58 encoded public key + + Returns: + :obj:`list` of :obj:`dict`: a list of unvoted blocks + """ + + unvoted = self.connection.run( + r.table('bigchain', read_mode=self.read_mode) + .filter(lambda block: r.table('votes', read_mode=self.read_mode) + .get_all([block['id'], node_pubkey], index='block_and_voter') + .is_empty()) + .order_by(r.asc(r.row['block']['timestamp']))) + + # FIXME: I (@vrde) don't like this solution. Filtering should be done at a + # database level. Solving issue #444 can help untangling the situation + unvoted_blocks = filter(lambda block: not util.is_genesis_block(block), unvoted) + return unvoted_blocks diff --git a/bigchaindb/db/utils.py b/bigchaindb/db/utils.py index 92e0fdd3..8e34fd99 100644 --- a/bigchaindb/db/utils.py +++ b/bigchaindb/db/utils.py @@ -67,6 +67,18 @@ class Connection: time.sleep(2**i) +def get_backend(host=None, port=None, db=None): + '''Get a backend instance.''' + + from bigchaindb.db.backends import rethinkdb + + # NOTE: this function will be re-implemented when we have real + # multiple backends to support. Right now it returns the RethinkDB one. + return rethinkdb.RethinkDBBackend(host=host or bigchaindb.config['database']['host'], + port=port or bigchaindb.config['database']['port'], + db=db or bigchaindb.config['database']['name']) + + def get_conn(): '''Get the connection to the database.''' @@ -149,10 +161,7 @@ def create_votes_secondary_index(conn, dbname): r.db(dbname).table('votes').index_wait().run(conn) -def init(): - # Try to access the keypair, throws an exception if it does not exist - b = bigchaindb.Bigchain() - +def init_database(): conn = get_conn() dbname = get_database_name() create_database(conn, dbname) @@ -160,10 +169,18 @@ def init(): table_names = ['bigchain', 'backlog', 'votes'] for table_name in table_names: create_table(conn, dbname, table_name) + create_bigchain_secondary_index(conn, dbname) create_backlog_secondary_index(conn, dbname) create_votes_secondary_index(conn, dbname) + +def init(): + # Try to access the keypair, throws an exception if it does not exist + b = bigchaindb.Bigchain() + + init_database() + logger.info('Create genesis block.') b.create_genesis_block() logger.info('Done, have fun!') @@ -172,9 +189,9 @@ def init(): def drop(assume_yes=False): conn = get_conn() dbname = bigchaindb.config['database']['name'] - if assume_yes: response = 'y' + else: response = input('Do you want to drop `{}` database? [y/n]: '.format(dbname)) @@ -185,5 +202,6 @@ def drop(assume_yes=False): logger.info('Done.') except r.ReqlOpFailedError: raise exceptions.DatabaseDoesNotExist('Database `{}` does not exist'.format(dbname)) + else: logger.info('Drop aborted') diff --git a/bigchaindb/models.py b/bigchaindb/models.py index 87ea57bb..cd472ed6 100644 --- a/bigchaindb/models.py +++ b/bigchaindb/models.py @@ -3,41 +3,11 @@ from bigchaindb.common.exceptions import (InvalidHash, InvalidSignature, OperationError, DoubleSpend, TransactionDoesNotExist, TransactionNotInValidBlock, - AssetIdMismatch) + 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. @@ -73,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 ' @@ -81,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 @@ -103,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 {}.' @@ -120,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: @@ -148,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 @@ -184,6 +209,15 @@ class Block(object): return self def sign(self, signing_key): + """Create a signature for the Block and overwrite `self.signature`. + + Args: + signing_key (str): A signing key corresponding to + `self.node_pubkey`. + + Returns: + :class:`~.Block` + """ block_body = self.to_dict() block_serialized = serialize(block_body['block']) signing_key = SigningKey(signing_key) @@ -191,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() verifying_key = VerifyingKey(block['node_pubkey']) try: @@ -204,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) @@ -222,7 +276,7 @@ class Block(object): # https://github.com/bigchaindb/cryptoconditions/issues/27 try: signature_valid = verifying_key\ - .verify(block_serialized.encode(), signature) + .verify(block_serialized.encode(), signature) except ValueError: signature_valid = False if signature_valid is False: @@ -239,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/block.py b/bigchaindb/pipelines/block.py index 0d5e24b2..4142b234 100644 --- a/bigchaindb/pipelines/block.py +++ b/bigchaindb/pipelines/block.py @@ -69,10 +69,7 @@ class BlockPipeline: # if the tx is already in a valid or undecided block, # then it no longer should be in the backlog, or added # to a new block. We can delete and drop it. - self.bigchain.connection.run( - r.table('backlog') - .get(tx.id) - .delete(durability='hard')) + self.bigchain.delete_transaction(tx.id) return None tx_validated = self.bigchain.is_valid_transaction(tx) @@ -81,10 +78,7 @@ class BlockPipeline: else: # if the transaction is not valid, remove it from the # backlog - self.bigchain.connection.run( - r.table('backlog') - .get(tx.id) - .delete(durability='hard')) + self.bigchain.delete_transaction(tx.id) return None def create(self, tx, timeout=False): @@ -136,10 +130,7 @@ class BlockPipeline: Returns: :class:`~bigchaindb.models.Block`: The block. """ - self.bigchain.connection.run( - r.table('backlog') - .get_all(*[tx.id for tx in block.transactions]) - .delete(durability='hard')) + self.bigchain.delete_transaction(*[tx.id for tx in block.transactions]) return block diff --git a/bigchaindb/pipelines/utils.py b/bigchaindb/pipelines/utils.py index 26984500..71f740ee 100644 --- a/bigchaindb/pipelines/utils.py +++ b/bigchaindb/pipelines/utils.py @@ -73,4 +73,3 @@ class ChangeFeed(Node): self.outqueue.put(change['old_val']) elif is_update and (self.operation & ChangeFeed.UPDATE): self.outqueue.put(change['new_val']) - 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/views/base.py b/bigchaindb/web/views/base.py index 82fdc418..9282b5b9 100644 --- a/bigchaindb/web/views/base.py +++ b/bigchaindb/web/views/base.py @@ -12,4 +12,3 @@ def make_error(status_code, message=None): }) response.status_code = status_code return response - diff --git a/deploy-cluster-aws/example_deploy_conf.py b/deploy-cluster-aws/example_deploy_conf.py index a36cd6e6..5d22e52b 100644 --- a/deploy-cluster-aws/example_deploy_conf.py +++ b/deploy-cluster-aws/example_deploy_conf.py @@ -44,9 +44,8 @@ USE_KEYPAIRS_FILE=False # and you can search for one that meets your needs at: # https://cloud-images.ubuntu.com/locator/ec2/ # Example: -# "ami-72c33e1d" -# (eu-central-1 Ubuntu 14.04 LTS amd64 hvm:ebs-ssd 20160919) -IMAGE_ID="ami-72c33e1d" +# (eu-central-1 Ubuntu 14.04 LTS amd64 hvm:ebs-ssd 20161020) +IMAGE_ID="ami-9c09f0f3" # INSTANCE_TYPE is the type of AWS instance to launch # i.e. How many CPUs do you want? How much storage? etc. diff --git a/docs/root/source/data-models/transaction-model.md b/docs/root/source/data-models/transaction-model.md index 881ab8b4..f8cb5929 100644 --- a/docs/root/source/data-models/transaction-model.md +++ b/docs/root/source/data-models/transaction-model.md @@ -28,11 +28,11 @@ Here's some explanation of the contents of a transaction: - `fulfillments`: List of fulfillments. Each _fulfillment_ contains a pointer to an unspent asset and a _crypto fulfillment_ that satisfies a spending condition set on the unspent asset. A _fulfillment_ is usually a signature proving the ownership of the asset. - See [Conditions and Fulfillments](#conditions-and-fulfillments) below. + See the page about [Crypto-Conditions and Fulfillments](crypto-conditions.html). - `conditions`: List of conditions. Each _condition_ is a _crypto-condition_ that needs to be fulfilled by a transfer transaction in order to transfer ownership to new owners. - See [Conditions and Fulfillments](#conditions-and-fulfillments) below. + See the page about [Crypto-Conditions and Fulfillments](crypto-conditions.html). - `operation`: String representation of the operation being performed (currently either "CREATE", "TRANSFER" or "GENESIS"). It determines how the transaction should be validated. - - `timestamp`: The Unix time when the transaction was created. It's provided by the client. See [the section on timestamps](timestamps.html). + - `timestamp`: The Unix time when the transaction was created. It's provided by the client. See the page about [timestamps in BigchainDB](../timestamps.html). - `asset`: Definition of the digital asset. See next section. - `metadata`: - `id`: UUID version 4 (random) converted to a string of hex digits in standard form. diff --git a/docs/root/source/transaction-concepts.md b/docs/root/source/transaction-concepts.md index 718915ec..541cd886 100644 --- a/docs/root/source/transaction-concepts.md +++ b/docs/root/source/transaction-concepts.md @@ -12,11 +12,18 @@ A creation transaction also establishes the conditions that must be met to trans A _transfer transaction_ can transfer an asset by fulfilling the current conditions on the asset. It can also specify new transfer conditions. -Today, every transaction contains one fulfillment-condition pair. The fulfillment in a transfer transaction must correspond to a condition in a previous transaction. +Today, every transaction contains one fulfillment-condition pair. The fulfillment in a transfer transaction must fulfill a condition in a previous transaction. -When a node is asked to check the validity of a transaction, it must do several things, including: +When a node is asked to check if a transaction is valid, it checks several things. Some things it checks are: -* double-spending checks (for transfer transactions), -* hash validation (i.e. is the calculated transaction hash equal to its id?), and -* validation of all fulfillments, including validation of cryptographic signatures if they’re among the conditions. +* Are all the fulfillments valid? (Do they correctly satisfy the conditions they claim to satisfy?) +* If it's a creation transaction, is the asset valid? +* If it's a transfer transaction: + * Is it trying to fulfill a condition in a nonexistent transaction? + * Is it trying to fulfill a condition that's not in a valid transaction? (It's okay if the condition is in a transaction in an invalid block; those transactions are ignored. Transactions in the backlog or undecided blocks are not ignored.) + * Is it trying to fulfill a condition that has already been fulfilled, or that some other pending transaction (in the backlog or an undecided block) also aims to fulfill? + * Is the asset ID in the transaction the same as the asset ID in all transactions whose conditions are being fulfilled? +If you're curious about the details of transaction validation, the code is in the `validate` method of the `Transaction` class, in `bigchaindb/models.py` (at the time of writing). + +Note: The check to see if the transaction ID is equal to the hash of the transaction body is actually done whenever the transaction is converted from a Python dict to a Transaction object, which must be done before the `validate` method can be called (since it's called on a Transaction object). diff --git a/docs/server/source/clusters-feds/aws-testing-cluster.md b/docs/server/source/clusters-feds/aws-testing-cluster.md index 61557102..e829fcbb 100644 --- a/docs/server/source/clusters-feds/aws-testing-cluster.md +++ b/docs/server/source/clusters-feds/aws-testing-cluster.md @@ -126,7 +126,7 @@ BRANCH="master" WHAT_TO_DEPLOY="servers" SSH_KEY_NAME="not-set-yet" USE_KEYPAIRS_FILE=False -IMAGE_ID="ami-72c33e1d" +IMAGE_ID="ami-9c09f0f3" INSTANCE_TYPE="t2.medium" SECURITY_GROUP="bigchaindb" USING_EBS=True diff --git a/docs/server/source/conf.py b/docs/server/source/conf.py index 640e3273..ab9531fd 100644 --- a/docs/server/source/conf.py +++ b/docs/server/source/conf.py @@ -41,7 +41,7 @@ extensions = [ 'sphinx.ext.intersphinx', 'sphinx.ext.coverage', 'sphinx.ext.viewcode', - 'sphinxcontrib.napoleon', + 'sphinx.ext.napoleon', 'sphinxcontrib.httpdomain', ] diff --git a/docs/server/source/drivers-clients/http-client-server-api.rst b/docs/server/source/drivers-clients/http-client-server-api.rst index 74bcf1d2..d274efad 100644 --- a/docs/server/source/drivers-clients/http-client-server-api.rst +++ b/docs/server/source/drivers-clients/http-client-server-api.rst @@ -3,21 +3,35 @@ The HTTP Client-Server API .. note:: - The HTTP client-server API is currently quite rudimentary. For example, there is no ability to do complex queries using the HTTP API. We plan to add querying capabilities in the future. + The HTTP client-server API is currently quite rudimentary. For example, + there is no ability to do complex queries using the HTTP API. We plan to add + querying capabilities in the future. -When you start Bigchaindb using `bigchaindb start`, an HTTP API is exposed at the address stored in the BigchainDB node configuration settings. The default is: +When you start Bigchaindb using `bigchaindb start`, an HTTP API is exposed at +the address stored in the BigchainDB node configuration settings. The default +is: `http://localhost:9984/api/v1/ `_ -but that address can be changed by changing the "API endpoint" configuration setting (e.g. in a local config file). There's more information about setting the API endpoint in :doc:`the section about BigchainDB Configuration Settings <../server-reference/configuration>`. +but that address can be changed by changing the "API endpoint" configuration +setting (e.g. in a local config file). There's more information about setting +the API endpoint in :doc:`the section about BigchainDB Configuration Settings +<../server-reference/configuration>`. -There are other configuration settings related to the web server (serving the HTTP API). In particular, the default is for the web server socket to bind to ``localhost:9984`` but that can be changed (e.g. to ``0.0.0.0:9984``). For more details, see the "server" settings ("bind", "workers" and "threads") in :doc:`the section about BigchainDB Configuration Settings <../server-reference/configuration>`. +There are other configuration settings related to the web server (serving the +HTTP API). In particular, the default is for the web server socket to bind to +``localhost:9984`` but that can be changed (e.g. to ``0.0.0.0:9984``). For more +details, see the "server" settings ("bind", "workers" and "threads") in +:doc:`the section about BigchainDB Configuration Settings +<../server-reference/configuration>`. API Root -------- -If you send an HTTP GET request to e.g. ``http://localhost:9984`` (with no ``/api/v1/`` on the end), then you should get an HTTP response with something like the following in the body: +If you send an HTTP GET request to e.g. ``http://localhost:9984`` (with no +``/api/v1/`` on the end), then you should get an HTTP response with something +like the following in the body: .. code-block:: json @@ -39,8 +53,13 @@ POST /transactions/ .. http:post:: /transactions/ Push a new transaction. - - Note: The posted transaction should be valid `transaction `_. The steps to build a valid transaction are beyond the scope of this page. One would normally use a driver such as the `BigchainDB Python Driver `_ to build a valid transaction. + + Note: The posted transaction should be valid `transaction + `_. + The steps to build a valid transaction are beyond the scope of this page. + One would normally use a driver such as the `BigchainDB Python Driver + `_ to + build a valid transaction. **Example request**: @@ -158,9 +177,11 @@ GET /transactions/{tx_id}/status .. http:get:: /transactions/{tx_id}/status - Get the status of the transaction with the ID ``tx_id``, if a transaction with that ``tx_id`` exists. + Get the status of the transaction with the ID ``tx_id``, if a transaction + with that ``tx_id`` exists. - The possible status values are ``backlog``, ``undecided``, ``valid`` or ``invalid``. + The possible status values are ``backlog``, ``undecided``, ``valid`` or + ``invalid``. :param tx_id: transaction ID :type tx_id: hex string @@ -194,7 +215,8 @@ GET /transactions/{tx_id} Get the transaction with the ID ``tx_id``. - This endpoint returns only a transaction from a ``VALID`` or ``UNDECIDED`` block on ``bigchain``, if exists. + This endpoint returns only a transaction from a ``VALID`` or ``UNDECIDED`` + block on ``bigchain``, if exists. :param tx_id: transaction ID :type tx_id: hex string @@ -260,4 +282,4 @@ GET /transactions/{tx_id} } :statuscode 200: A transaction with that ID was found. - :statuscode 404: A transaction with that ID was not found. \ No newline at end of file + :statuscode 404: A transaction with that ID was not found. diff --git a/setup.py b/setup.py index 67a367f6..0f558dd7 100644 --- a/setup.py +++ b/setup.py @@ -31,10 +31,10 @@ check_setuptools_features() tests_require = [ 'coverage', 'pep8', - 'pyflakes', + 'flake8', 'pylint', 'pytest', - 'pytest-cov==2.2.1', + 'pytest-cov>=2.2.1', 'pytest-xdist', 'pytest-flask', ] @@ -48,7 +48,6 @@ docs_require = [ 'Sphinx>=1.3.5', 'recommonmark>=0.4.0', 'sphinx-rtd-theme>=0.1.9', - 'sphinxcontrib-napoleon>=0.4.4', 'sphinxcontrib-httpdomain>=1.5.0', ] @@ -56,6 +55,20 @@ benchmarks_require = [ 'line-profiler==1.0', ] +install_requires = [ + 'rethinkdb~=2.3', # i.e. a version between 2.3 and 3.0 + 'pysha3>=0.3', + 'cryptoconditions>=0.5.0', + 'statsd>=3.2.1', + 'python-rapidjson>=0.0.6', + 'logstats>=0.2.1', + 'flask>=0.10.1', + 'flask-restful~=0.3.0', + 'requests~=2.9', + 'gunicorn~=19.0', + 'multipipes~=0.1.0', +] + setup( name='BigchainDB', version=version['__version__'], @@ -75,7 +88,7 @@ setup( 'Topic :: Software Development', 'Natural Language :: English', 'License :: OSI Approved :: GNU Affero General Public License v3', - 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3 :: Only', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Operating System :: MacOS :: MacOS X', @@ -89,21 +102,7 @@ setup( 'bigchaindb=bigchaindb.commands.bigchain:main' ], }, - install_requires=[ - 'rethinkdb~=2.3', - 'pysha3==0.3', - 'pytz==2015.7', - 'cryptoconditions==0.5.0', - 'statsd==3.2.1', - 'python-rapidjson==0.0.6', - 'logstats==0.2.1', - 'base58==0.2.2', - 'flask==0.10.1', - 'flask-restful~=0.3.0', - 'requests~=2.9', - 'gunicorn~=19.0', - 'multipipes~=0.1.0', - ], + install_requires=install_requires, setup_requires=['pytest-runner'], tests_require=tests_require, extras_require={ diff --git a/tests/assets/test_digital_assets.py b/tests/assets/test_digital_assets.py index e18684c5..56a08d53 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_vk, user_sk): tx_input = b.get_owned_ids(user_vk).pop() tx_create = b.get_transaction(tx_input.txid) - tx_transfer = Transaction.transfer(tx_create.to_inputs(), [user_vk], + tx_transfer = Transaction.transfer(tx_create.to_inputs(), [([user_vk], 1)], tx_create.asset) tx_transfer_signed = tx_transfer.sign([user_sk]) @@ -18,61 +20,40 @@ def test_asset_transfer(b, user_vk, user_sk): def test_validate_bad_asset_creation(b, user_vk): - from bigchaindb.models import Transaction + from bigchaindb.models import Transaction, Asset # `divisible` needs to be a boolean - tx = Transaction.create([b.me], [user_vk]) + tx = Transaction.create([b.me], [([user_vk], 1)]) tx.asset.divisible = 1 - tx_signed = tx.sign([b.me_private]) + with patch.object(Asset, 'validate_asset', return_value=None): + tx_signed = tx.sign([b.me_private]) with pytest.raises(TypeError): tx_signed.validate(b) # `refillable` needs to be a boolean - tx = Transaction.create([b.me], [user_vk]) + tx = Transaction.create([b.me], [([user_vk], 1)]) tx.asset.refillable = 1 - tx_signed = tx.sign([b.me_private]) + with patch.object(Asset, 'validate_asset', return_value=None): + tx_signed = tx.sign([b.me_private]) with pytest.raises(TypeError): b.validate_transaction(tx_signed) # `updatable` needs to be a boolean - tx = Transaction.create([b.me], [user_vk]) + tx = Transaction.create([b.me], [([user_vk], 1)]) tx.asset.updatable = 1 - tx_signed = tx.sign([b.me_private]) + with patch.object(Asset, 'validate_asset', return_value=None): + tx_signed = tx.sign([b.me_private]) with pytest.raises(TypeError): b.validate_transaction(tx_signed) # `data` needs to be a dictionary - tx = Transaction.create([b.me], [user_vk]) + tx = Transaction.create([b.me], [([user_vk], 1)]) tx.asset.data = 'a' - tx_signed = tx.sign([b.me_private]) + with patch.object(Asset, 'validate_asset', return_value=None): + tx_signed = tx.sign([b.me_private]) with pytest.raises(TypeError): b.validate_transaction(tx_signed) - # TODO: Check where to test for the amount - """ - tx = b.create_transaction(b.me, user_vk, None, 'CREATE') - tx['transaction']['conditions'][0]['amount'] = 'a' - tx['id'] = get_hash_data(tx['transaction']) - tx_signed = b.sign_transaction(tx, b.me_private) - with pytest.raises(TypeError): - b.validate_transaction(tx_signed) - - tx = b.create_transaction(b.me, user_vk, None, 'CREATE') - tx['transaction']['conditions'][0]['amount'] = 2 - tx['transaction']['asset'].update({'divisible': False}) - tx['id'] = get_hash_data(tx['transaction']) - tx_signed = b.sign_transaction(tx, b.me_private) - with pytest.raises(AmountError): - b.validate_transaction(tx_signed) - - tx = b.create_transaction(b.me, user_vk, None, 'CREATE') - tx['transaction']['conditions'][0]['amount'] = 0 - tx['id'] = get_hash_data(tx['transaction']) - tx_signed = b.sign_transaction(tx, b.me_private) - with pytest.raises(AmountError): - b.validate_transaction(tx_signed) - """ - @pytest.mark.usefixtures('inputs') def test_validate_transfer_asset_id_mismatch(b, user_vk, user_sk): @@ -81,7 +62,7 @@ def test_validate_transfer_asset_id_mismatch(b, user_vk, user_sk): tx_create = b.get_owned_ids(user_vk).pop() tx_create = b.get_transaction(tx_create.txid) - tx_transfer = Transaction.transfer(tx_create.to_inputs(), [user_vk], + tx_transfer = Transaction.transfer(tx_create.to_inputs(), [([user_vk], 1)], tx_create.asset) tx_transfer.asset.data_id = 'aaa' tx_transfer_signed = tx_transfer.sign([user_sk]) @@ -92,7 +73,7 @@ def test_validate_transfer_asset_id_mismatch(b, user_vk, user_sk): def test_get_asset_id_create_transaction(b, user_vk): from bigchaindb.models import Transaction, Asset - tx_create = Transaction.create([b.me], [user_vk]) + tx_create = Transaction.create([b.me], [([user_vk], 1)]) asset_id = Asset.get_asset_id(tx_create) assert asset_id == tx_create.asset.data_id @@ -105,7 +86,7 @@ def test_get_asset_id_transfer_transaction(b, user_vk, user_sk): tx_create = b.get_owned_ids(user_vk).pop() tx_create = b.get_transaction(tx_create.txid) # create a transfer transaction - tx_transfer = Transaction.transfer(tx_create.to_inputs(), [user_vk], + tx_transfer = Transaction.transfer(tx_create.to_inputs(), [([user_vk], 1)], tx_create.asset) tx_transfer_signed = tx_transfer.sign([user_sk]) # create a block @@ -123,8 +104,8 @@ def test_asset_id_mismatch(b, user_vk): from bigchaindb.models import Transaction, Asset from bigchaindb.common.exceptions import AssetIdMismatch - tx1 = Transaction.create([b.me], [user_vk]) - tx2 = Transaction.create([b.me], [user_vk]) + tx1 = Transaction.create([b.me], [([user_vk], 1)]) + tx2 = Transaction.create([b.me], [([user_vk], 1)]) with pytest.raises(AssetIdMismatch): Asset.get_asset_id([tx1, tx2]) @@ -144,7 +125,7 @@ def test_get_txs_by_asset_id(b, user_vk, user_sk): assert txs[0].asset.data_id == asset_id # create a transfer transaction - tx_transfer = Transaction.transfer(tx_create.to_inputs(), [user_vk], + tx_transfer = Transaction.transfer(tx_create.to_inputs(), [([user_vk], 1)], tx_create.asset) tx_transfer_signed = tx_transfer.sign([user_sk]) # create the block @@ -161,3 +142,73 @@ def test_get_txs_by_asset_id(b, user_vk, user_sk): assert tx_transfer.id in [t.id for t in txs] assert asset_id == txs[0].asset.data_id assert asset_id == txs[1].asset.data_id + + +@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 5f2d58fb..baba35af 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,62 @@ def test_cast_transaction_link_to_boolean(): assert bool(TransactionLink(False, False)) is True +def test_asset_link_serialization(): + from bigchaindb.common.transaction import AssetLink + + data_id = 'a asset id' + expected = { + 'id': data_id, + } + asset_link = AssetLink(data_id) + + assert asset_link.to_dict() == expected + + +def test_asset_link_serialization_with_empty_payload(): + from bigchaindb.common.transaction import AssetLink + + expected = None + asset_link = AssetLink() + + assert asset_link.to_dict() == expected + + +def test_asset_link_deserialization(): + from bigchaindb.common.transaction import AssetLink + + data_id = 'a asset id' + expected = AssetLink(data_id) + asset_link = { + 'id': data_id + } + asset_link = AssetLink.from_dict(asset_link) + + assert asset_link == expected + + +def test_asset_link_deserialization_with_empty_payload(): + from bigchaindb.common.transaction import AssetLink + + expected = AssetLink() + asset_link = AssetLink.from_dict(None) + + assert asset_link == expected + + +def test_cast_asset_link_to_boolean(): + from bigchaindb.common.transaction import AssetLink + + assert bool(AssetLink()) is False + assert bool(AssetLink('a')) is True + assert bool(AssetLink(False)) is True + + def test_add_fulfillment_to_tx(user_ffill): from bigchaindb.common.transaction import Transaction, Asset - 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 +529,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 +538,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 +548,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 +624,14 @@ def test_validate_multiple_fulfillments(user_ffill, user_cond, user_priv): from bigchaindb.common.crypto import SigningKey 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 +682,7 @@ def test_multiple_fulfillment_validation_of_transfer_tx(user_ffill, user_cond, Fulfillment, Condition, Asset) from cryptoconditions import Ed25519Fulfillment - tx = Transaction(Transaction.CREATE, Asset(), + tx = Transaction(Transaction.CREATE, Asset(divisible=True), [user_ffill, deepcopy(user_ffill)], [user_cond, deepcopy(user_cond)]) tx.sign([user_priv]) @@ -715,10 +723,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 +758,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 +771,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 +793,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 +857,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 +872,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 +938,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 +957,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 +979,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 +989,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 +998,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 +1030,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 58178b7f..fcb6b318 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -72,7 +72,7 @@ def b(request, node_config): @pytest.fixture def create_tx(b, user_vk): from bigchaindb.models import Transaction - return Transaction.create([b.me], [user_vk]) + return Transaction.create([b.me], [([user_vk], 1)]) @pytest.fixture @@ -84,5 +84,5 @@ def signed_create_tx(b, create_tx): def signed_transfer_tx(signed_create_tx, user_vk, user_sk): from bigchaindb.models import Transaction inputs = signed_create_tx.to_inputs() - tx = Transaction.transfer(inputs, [user_vk], signed_create_tx.asset) + tx = Transaction.transfer(inputs, [([user_vk], 1)], signed_create_tx.asset) return tx.sign([user_sk]) diff --git a/tests/db/conftest.py b/tests/db/conftest.py index f55a4c34..43a352a9 100644 --- a/tests/db/conftest.py +++ b/tests/db/conftest.py @@ -10,11 +10,13 @@ import pytest import rethinkdb as r from bigchaindb import Bigchain -from bigchaindb.db import get_conn +from bigchaindb.db import get_conn, init_database from bigchaindb.common import crypto +from bigchaindb.common.exceptions import DatabaseAlreadyExists USER2_SK, USER2_VK = crypto.generate_key_pair() + @pytest.fixture(autouse=True) def restore_config(request, node_config): from bigchaindb import config_utils @@ -25,53 +27,17 @@ def restore_config(request, node_config): def setup_database(request, node_config): print('Initializing test db') db_name = node_config['database']['name'] - get_conn().repl() + conn = get_conn() + + if r.db_list().contains(db_name).run(conn): + r.db_drop(db_name).run(conn) + try: - r.db_create(db_name).run() - except r.ReqlOpFailedError as e: - if e.message == 'Database `{}` already exists.'.format(db_name): - r.db_drop(db_name).run() - r.db_create(db_name).run() - else: - raise + init_database() + except DatabaseAlreadyExists: + print('Database already exists.') - print('Finished initializing test db') - - # setup tables - r.db(db_name).table_create('bigchain').run() - r.db(db_name).table_create('backlog').run() - r.db(db_name).table_create('votes').run() - - # create the secondary indexes - # to order blocks by timestamp - r.db(db_name).table('bigchain').index_create('block_timestamp', r.row['block']['timestamp']).run() - # to order blocks by block number - r.db(db_name).table('bigchain').index_create('block_number', r.row['block']['block_number']).run() - # to order transactions by timestamp - r.db(db_name).table('backlog').index_create('transaction_timestamp', r.row['transaction']['timestamp']).run() - # to query by payload uuid - r.db(db_name).table('bigchain').index_create( - 'metadata_id', - r.row['block']['transactions']['transaction']['metadata']['id'], - multi=True, - ).run() - # compound index to read transactions from the backlog per assignee - r.db(db_name).table('backlog')\ - .index_create('assignee__transaction_timestamp', [r.row['assignee'], r.row['transaction']['timestamp']])\ - .run() - # compound index to order votes by block id and node - r.db(db_name).table('votes').index_create('block_and_voter', - [r.row['vote']['voting_for_block'], r.row['node_pubkey']]).run() - # secondary index for asset uuid - r.db(db_name).table('bigchain')\ - .index_create('asset_id', - r.row['block']['transactions']['transaction']['asset']['id'], multi=True)\ - .run() - # order transactions by id - r.db(db_name).table('bigchain').index_create('transaction_id', r.row['block']['transactions']['id'], - multi=True).run() - - r.db(db_name).table('bigchain').index_wait('transaction_id').run() + print('Finishing init database') def fin(): print('Deleting `{}` database'.format(db_name)) @@ -81,7 +47,6 @@ def setup_database(request, node_config): except r.ReqlOpFailedError as e: if e.message != 'Database `{}` does not exist.'.format(db_name): raise - print('Finished deleting `{}`'.format(db_name)) request.addfinalizer(fin) @@ -119,7 +84,7 @@ def inputs(user_vk): prev_block_id = g.id for block in range(4): transactions = [ - Transaction.create([b.me], [user_vk]).sign([b.me_private]) + Transaction.create([b.me], [([user_vk], 1)]).sign([b.me_private]) for i in range(10) ] block = b.create_block(transactions) diff --git a/tests/db/test_bigchain_api.py b/tests/db/test_bigchain_api.py index b52dd3a7..144bfeed 100644 --- a/tests/db/test_bigchain_api.py +++ b/tests/db/test_bigchain_api.py @@ -15,7 +15,7 @@ def dummy_tx(): import bigchaindb from bigchaindb.models import Transaction b = bigchaindb.Bigchain() - tx = Transaction.create([b.me], [b.me]) + tx = Transaction.create([b.me], [([b.me], 1)]) tx = tx.sign([b.me_private]) return tx @@ -37,7 +37,7 @@ class TestBigchainApi(object): b.create_genesis_block() - tx = Transaction.create([b.me], [b.me]) + tx = Transaction.create([b.me], [([b.me], 1)]) tx = tx.sign([b.me_private]) monkeypatch.setattr('time.time', lambda: 1) block1 = b.create_block([tx]) @@ -60,7 +60,7 @@ class TestBigchainApi(object): b.create_genesis_block() - tx = Transaction.create([b.me], [b.me]) + tx = Transaction.create([b.me], [([b.me], 1)]) tx = tx.sign([b.me_private]) block1 = b.create_block([tx]) @@ -74,7 +74,7 @@ class TestBigchainApi(object): b.create_genesis_block() - tx = Transaction.create([b.me], [b.me]) + tx = Transaction.create([b.me], [([b.me], 1)]) tx = tx.sign([b.me_private]) monkeypatch.setattr('time.time', lambda: 1) @@ -99,7 +99,7 @@ class TestBigchainApi(object): b.create_genesis_block() - tx = Transaction.create([b.me], [b.me]) + tx = Transaction.create([b.me], [([b.me], 1)]) tx = tx.sign([b.me_private]) monkeypatch.setattr('time.time', lambda: 1) @@ -107,13 +107,15 @@ class TestBigchainApi(object): b.write_block(block1, durability='hard') monkeypatch.setattr('time.time', lambda: 2) - transfer_tx = Transaction.transfer(tx.to_inputs(), [b.me], tx.asset) + transfer_tx = Transaction.transfer(tx.to_inputs(), [([b.me], 1)], + tx.asset) transfer_tx = transfer_tx.sign([b.me_private]) block2 = b.create_block([transfer_tx]) b.write_block(block2, durability='hard') monkeypatch.setattr('time.time', lambda: 3) - transfer_tx2 = Transaction.transfer(tx.to_inputs(), [b.me], tx.asset) + transfer_tx2 = Transaction.transfer(tx.to_inputs(), [([b.me], 1)], + tx.asset) transfer_tx2 = transfer_tx2.sign([b.me_private]) block3 = b.create_block([transfer_tx2]) b.write_block(block3, durability='hard') @@ -133,7 +135,7 @@ class TestBigchainApi(object): b.create_genesis_block() - tx = Transaction.create([b.me], [b.me]) + tx = Transaction.create([b.me], [([b.me], 1)]) tx = tx.sign([b.me_private]) monkeypatch.setattr('time.time', lambda: 1) @@ -159,13 +161,13 @@ class TestBigchainApi(object): b.create_genesis_block() monkeypatch.setattr('time.time', lambda: 1) - tx1 = Transaction.create([b.me], [b.me]) + tx1 = Transaction.create([b.me], [([b.me], 1)]) tx1 = tx1.sign([b.me_private]) block1 = b.create_block([tx1]) b.write_block(block1, durability='hard') monkeypatch.setattr('time.time', lambda: 2) - tx2 = Transaction.create([b.me], [b.me]) + tx2 = Transaction.create([b.me], [([b.me], 1)]) tx2 = tx2.sign([b.me_private]) block2 = b.create_block([tx2]) b.write_block(block2, durability='hard') @@ -185,7 +187,7 @@ class TestBigchainApi(object): from bigchaindb.models import Transaction metadata = {'msg': 'Hello BigchainDB!'} - tx = Transaction.create([b.me], [user_vk], metadata=metadata) + tx = Transaction.create([b.me], [([user_vk], 1)], metadata=metadata) block = b.create_block([tx]) b.write_block(block, durability='hard') @@ -205,7 +207,7 @@ class TestBigchainApi(object): input_tx = b.get_owned_ids(user_vk).pop() input_tx = b.get_transaction(input_tx.txid) inputs = input_tx.to_inputs() - tx = Transaction.transfer(inputs, [user_vk], input_tx.asset) + tx = Transaction.transfer(inputs, [([user_vk], 1)], input_tx.asset) tx = tx.sign([user_sk]) response = b.write_transaction(tx) @@ -223,7 +225,7 @@ class TestBigchainApi(object): input_tx = b.get_owned_ids(user_vk).pop() input_tx = b.get_transaction(input_tx.txid) inputs = input_tx.to_inputs() - tx = Transaction.transfer(inputs, [user_vk], input_tx.asset) + tx = Transaction.transfer(inputs, [([user_vk], 1)], input_tx.asset) tx = tx.sign([user_sk]) b.write_transaction(tx) @@ -243,9 +245,9 @@ class TestBigchainApi(object): input_tx = b.get_owned_ids(user_vk).pop() input_tx = b.get_transaction(input_tx.txid) inputs = input_tx.to_inputs() - tx = Transaction.transfer(inputs, [user_vk], input_tx.asset) + tx = Transaction.transfer(inputs, [([user_vk], 1)], input_tx.asset) tx = tx.sign([user_sk]) - b.write_transaction(tx) + # There's no need to b.write_transaction(tx) to the backlog # create block block = b.create_block([tx]) @@ -257,8 +259,37 @@ class TestBigchainApi(object): response = b.get_transaction(tx.id) # should be None, because invalid blocks are ignored + # and a copy of the tx is not in the backlog assert response is None + @pytest.mark.usefixtures('inputs') + def test_read_transaction_invalid_block_and_backlog(self, b, user_vk, user_sk): + from bigchaindb.models import Transaction + + input_tx = b.get_owned_ids(user_vk).pop() + input_tx = b.get_transaction(input_tx.txid) + inputs = input_tx.to_inputs() + tx = Transaction.transfer(inputs, [([user_vk], 1)], input_tx.asset) + tx = tx.sign([user_sk]) + + # Make sure there's a copy of tx in the backlog + b.write_transaction(tx) + + # create block + block = b.create_block([tx]) + b.write_block(block, durability='hard') + + # vote the block invalid + vote = b.vote(block.id, b.get_last_voted_block().id, False) + b.write_vote(vote) + + # a copy of the tx is both in the backlog and in an invalid + # block, so get_transaction should return a transaction, + # and a status of TX_IN_BACKLOG + response, status = b.get_transaction(tx.id, include_status=True) + assert tx.to_dict() == response.to_dict() + assert status == b.TX_IN_BACKLOG + @pytest.mark.usefixtures('inputs') def test_genesis_block(self, b): import rethinkdb as r @@ -340,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 @@ -499,7 +539,7 @@ class TestBigchainApi(object): input_tx = b.get_owned_ids(user_vk).pop() input_tx = b.get_transaction(input_tx.txid) inputs = input_tx.to_inputs() - tx = Transaction.transfer(inputs, [user_vk], input_tx.asset) + tx = Transaction.transfer(inputs, [([user_vk], 1)], input_tx.asset) tx = tx.sign([user_sk]) b.write_transaction(tx) @@ -525,7 +565,7 @@ class TestBigchainApi(object): input_tx = b.get_owned_ids(user_vk).pop() input_tx = b.get_transaction(input_tx.txid) inputs = input_tx.to_inputs() - tx = Transaction.transfer(inputs, [user_vk], input_tx.asset) + tx = Transaction.transfer(inputs, [([user_vk], 1)], input_tx.asset) tx = tx.sign([user_sk]) b.write_transaction(tx) @@ -549,11 +589,21 @@ class TestBigchainApi(object): fulfillment = Fulfillment(Ed25519Fulfillment(public_key=user_vk), [user_vk], TransactionLink('somethingsomething', 0)) - tx = Transaction.transfer([fulfillment], [user_vk], Asset()) + tx = Transaction.transfer([fulfillment], [([user_vk], 1)], Asset()) - with pytest.raises(TransactionDoesNotExist) as excinfo: + 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_vk, create_tx): @@ -591,7 +641,7 @@ class TestTransactionValidation(object): input_tx = b.get_owned_ids(user_vk).pop() input_transaction = b.get_transaction(input_tx.txid) sk, vk = generate_key_pair() - tx = Transaction.create([vk], [user_vk]) + tx = Transaction.create([vk], [([user_vk], 1)]) tx.operation = 'TRANSFER' tx.asset = input_transaction.asset tx.fulfillments[0].tx_input = input_tx @@ -635,7 +685,8 @@ class TestTransactionValidation(object): input_tx = b.get_owned_ids(user_vk).pop() input_tx = b.get_transaction(input_tx.txid) inputs = input_tx.to_inputs() - transfer_tx = Transaction.transfer(inputs, [user_vk], input_tx.asset) + transfer_tx = Transaction.transfer(inputs, [([user_vk], 1)], + input_tx.asset) transfer_tx = transfer_tx.sign([user_sk]) assert transfer_tx == b.validate_transaction(transfer_tx) @@ -659,7 +710,8 @@ class TestTransactionValidation(object): inputs = input_tx.to_inputs() # create a transaction that's valid but not in a voted valid block - transfer_tx = Transaction.transfer(inputs, [user_vk], input_tx.asset) + transfer_tx = Transaction.transfer(inputs, [([user_vk], 1)], + input_tx.asset) transfer_tx = transfer_tx.sign([user_sk]) assert transfer_tx == b.validate_transaction(transfer_tx) @@ -669,8 +721,9 @@ class TestTransactionValidation(object): b.write_block(block, durability='hard') # create transaction with the undecided input - tx_invalid = Transaction.transfer(transfer_tx.to_inputs(), [user_vk], - transfer_tx.asset) + tx_invalid = Transaction.transfer(transfer_tx.to_inputs(), + [([user_vk], 1)], + transfer_tx.asset) tx_invalid = tx_invalid.sign([user_sk]) with pytest.raises(TransactionNotInValidBlock): @@ -768,7 +821,7 @@ class TestMultipleInputs(object): tx_link = b.get_owned_ids(user_vk).pop() input_tx = b.get_transaction(tx_link.txid) inputs = input_tx.to_inputs() - tx = Transaction.transfer(inputs, [user2_vk], input_tx.asset) + tx = Transaction.transfer(inputs, [([user2_vk], 1)], input_tx.asset) tx = tx.sign([user_sk]) # validate transaction @@ -776,69 +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_vk): - from bigchaindb.common import crypto - from bigchaindb.models import Transaction - - user2_sk, user2_vk = crypto.generate_key_pair() - - # get inputs - owned_inputs = b.get_owned_ids(user_vk) - input_txs = [b.get_transaction(tx_link.txid) for tx_link - in owned_inputs] - inputs = sum([input_tx.to_inputs() for input_tx in input_txs], []) - tx = Transaction.transfer(inputs, len(inputs) * [[user_vk]]) - tx = tx.sign([user_sk]) - assert b.validate_transaction(tx) == tx - assert len(tx.fulfillments) == len(inputs) - assert len(tx.conditions) == len(inputs) - - @pytest.mark.skipif(reason=('Multiple inputs are only allowed for the ' - 'same asset. Remove this after implementing ', - 'multiple assets')) - @pytest.mark.usefixtures('inputs') - def test_transfer_single_owners_single_input_from_multiple_outputs(self, b, - user_sk, - user_vk): - from bigchaindb.common import crypto - from bigchaindb.models import Transaction - - user2_sk, user2_vk = crypto.generate_key_pair() - - # get inputs - owned_inputs = b.get_owned_ids(user_vk) - input_txs = [b.get_transaction(tx_link.txid) for tx_link - in owned_inputs] - inputs = sum([input_tx.to_inputs() for input_tx in input_txs], []) - tx = Transaction.transfer(inputs, len(inputs) * [[user2_vk]]) - tx = tx.sign([user_sk]) - - # create block with the transaction - block = b.create_block([tx]) - b.write_block(block, durability='hard') - - # vote block valid - vote = b.vote(block.id, b.get_last_voted_block().id, True) - b.write_vote(vote) - - # get inputs from user2 - owned_inputs = b.get_owned_ids(user2_vk) - assert len(owned_inputs) == len(inputs) - - # create a transaction with a single input from a multiple output transaction - tx_link = owned_inputs.pop() - inputs = b.get_transaction(tx_link.txid).to_inputs([0]) - tx = Transaction.transfer(inputs, [user_vk]) - tx = tx.sign([user2_sk]) - - assert b.is_valid_transaction(tx) == tx - assert len(tx.fulfillments) == 1 - assert len(tx.conditions) == 1 - def test_single_owner_before_multiple_owners_after_single_input(self, b, user_sk, user_vk, @@ -852,47 +842,14 @@ class TestMultipleInputs(object): owned_inputs = b.get_owned_ids(user_vk) tx_link = owned_inputs.pop() input_tx = b.get_transaction(tx_link.txid) - tx = Transaction.transfer(input_tx.to_inputs(), [[user2_vk, user3_vk]], input_tx.asset) + tx = Transaction.transfer(input_tx.to_inputs(), + [([user2_vk, user3_vk], 1)], input_tx.asset) tx = tx.sign([user_sk]) assert b.is_valid_transaction(tx) == tx assert len(tx.fulfillments) == 1 assert len(tx.conditions) == 1 - @pytest.mark.skipif(reason=('Multiple inputs are only allowed for the ' - 'same asset. Remove this after implementing ', - 'multiple assets')) - @pytest.mark.usefixtures('inputs') - def test_single_owner_before_multiple_owners_after_multiple_inputs(self, b, - user_sk, - user_vk): - from bigchaindb.common import crypto - from bigchaindb.models import Transaction - - user2_sk, user2_vk = crypto.generate_key_pair() - user3_sk, user3_vk = crypto.generate_key_pair() - - owned_inputs = b.get_owned_ids(user_vk) - input_txs = [b.get_transaction(tx_link.txid) for tx_link - in owned_inputs] - inputs = sum([input_tx.to_inputs() for input_tx in input_txs], []) - - tx = Transaction.transfer(inputs, len(inputs) * [[user2_vk, user3_vk]]) - tx = tx.sign([user_sk]) - - # create block with the transaction - block = b.create_block([tx]) - b.write_block(block, durability='hard') - - # vote block valid - vote = b.vote(block.id, b.get_last_voted_block().id, True) - b.write_vote(vote) - - # validate transaction - assert b.is_valid_transaction(tx) == tx - assert len(tx.fulfillments) == len(inputs) - assert len(tx.conditions) == len(inputs) - @pytest.mark.usefixtures('inputs') def test_multiple_owners_before_single_owner_after_single_input(self, b, user_sk, @@ -903,7 +860,7 @@ class TestMultipleInputs(object): user2_sk, user2_vk = crypto.generate_key_pair() user3_sk, user3_vk = crypto.generate_key_pair() - tx = Transaction.create([b.me], [user_vk, user2_vk]) + tx = Transaction.create([b.me], [([user_vk, user2_vk], 1)]) tx = tx.sign([b.me_private]) block = b.create_block([tx]) b.write_block(block, durability='hard') @@ -916,7 +873,8 @@ class TestMultipleInputs(object): input_tx = b.get_transaction(owned_input.txid) inputs = input_tx.to_inputs() - transfer_tx = Transaction.transfer(inputs, [user3_vk], input_tx.asset) + transfer_tx = Transaction.transfer(inputs, [([user3_vk], 1)], + input_tx.asset) transfer_tx = transfer_tx.sign([user_sk, user2_sk]) # validate transaction @@ -924,29 +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_vk, user2_vk, user2_sk): - from bigchaindb.common import crypto - from bigchaindb.models import Transaction - - # create a new users - user3_sk, user3_vk = crypto.generate_key_pair() - - tx_links = b.get_owned_ids(user_vk) - inputs = sum([b.get_transaction(tx_link.txid).to_inputs() for tx_link - in tx_links], []) - - tx = Transaction.transfer(inputs, len(inputs) * [[user3_vk]]) - tx = tx.sign([user_sk, user2_sk]) - - assert b.is_valid_transaction(tx) == tx - assert len(tx.fulfillments) == len(inputs) - assert len(tx.conditions) == len(inputs) - @pytest.mark.usefixtures('inputs') def test_multiple_owners_before_multiple_owners_after_single_input(self, b, user_sk, @@ -958,7 +893,7 @@ class TestMultipleInputs(object): user3_sk, user3_vk = crypto.generate_key_pair() user4_sk, user4_vk = crypto.generate_key_pair() - tx = Transaction.create([b.me], [user_vk, user2_vk]) + tx = Transaction.create([b.me], [([user_vk, user2_vk], 1)]) tx = tx.sign([b.me_private]) block = b.create_block([tx]) b.write_block(block, durability='hard') @@ -971,38 +906,14 @@ class TestMultipleInputs(object): tx_link = b.get_owned_ids(user_vk).pop() tx_input = b.get_transaction(tx_link.txid) - tx = Transaction.transfer(tx_input.to_inputs(), [[user3_vk, user4_vk]], tx_input.asset) + tx = Transaction.transfer(tx_input.to_inputs(), + [([user3_vk, user4_vk], 1)], tx_input.asset) tx = tx.sign([user_sk, user2_sk]) assert b.is_valid_transaction(tx) == tx assert len(tx.fulfillments) == 1 assert len(tx.conditions) == 1 - @pytest.mark.skipif(reason=('Multiple inputs are only allowed for the ' - 'same asset. Remove this after implementing ', - 'multiple assets')) - @pytest.mark.usefixtures('inputs_shared') - def test_multiple_owners_before_multiple_owners_after_multiple_inputs(self, b, - user_sk, user_vk, - user2_sk, user2_vk): - from bigchaindb.common import crypto - from bigchaindb.models import Transaction - - # create a new users - user3_sk, user3_vk = crypto.generate_key_pair() - user4_sk, user4_vk = crypto.generate_key_pair() - - tx_links = b.get_owned_ids(user_vk) - inputs = sum([b.get_transaction(tx_link.txid).to_inputs() for tx_link - in tx_links], []) - - tx = Transaction.transfer(inputs, len(inputs) * [[user3_vk, user4_vk]]) - tx = tx.sign([user_sk, user2_sk]) - - assert b.is_valid_transaction(tx) == tx - assert len(tx.fulfillments) == len(inputs) - assert len(tx.conditions) == len(inputs) - def test_get_owned_ids_single_tx_single_output(self, b, user_sk, user_vk): from bigchaindb.common import crypto from bigchaindb.common.transaction import TransactionLink @@ -1010,7 +921,7 @@ class TestMultipleInputs(object): user2_sk, user2_vk = crypto.generate_key_pair() - tx = Transaction.create([b.me], [user_vk]) + tx = Transaction.create([b.me], [([user_vk], 1)]) tx = tx.sign([b.me_private]) block = b.create_block([tx]) b.write_block(block, durability='hard') @@ -1020,7 +931,7 @@ class TestMultipleInputs(object): assert owned_inputs_user1 == [TransactionLink(tx.id, 0)] assert owned_inputs_user2 == [] - tx = Transaction.transfer(tx.to_inputs(), [user2_vk], tx.asset) + tx = Transaction.transfer(tx.to_inputs(), [([user2_vk], 1)], tx.asset) tx = tx.sign([user_sk]) block = b.create_block([tx]) b.write_block(block, durability='hard') @@ -1040,7 +951,7 @@ class TestMultipleInputs(object): genesis = b.create_genesis_block() user2_sk, user2_vk = crypto.generate_key_pair() - tx = Transaction.create([b.me], [user_vk]) + tx = Transaction.create([b.me], [([user_vk], 1)]) tx = tx.sign([b.me_private]) block = b.create_block([tx]) b.write_block(block, durability='hard') @@ -1056,7 +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_vk], tx.asset) + tx_invalid = Transaction.transfer(tx.to_inputs(), [([user2_vk], 1)], + tx.asset) tx_invalid = tx_invalid.sign([user_sk]) block = b.create_block([tx_invalid]) b.write_block(block, durability='hard') @@ -1072,47 +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_vk): - import random from bigchaindb.common import crypto - from bigchaindb.common.transaction import TransactionLink + from bigchaindb.common.transaction import TransactionLink, Asset from bigchaindb.models import Transaction user2_sk, user2_vk = crypto.generate_key_pair() - transactions = [] - for i in range(2): - payload = {'somedata': random.randint(0, 255)} - tx = Transaction.create([b.me], [user_vk], payload) - tx = tx.sign([b.me_private]) - transactions.append(tx) - block = b.create_block(transactions) + # create divisible asset + asset = Asset(divisible=True) + tx_create = Transaction.create([b.me], + [([user_vk], 1), ([user_vk], 1)], + asset=asset) + tx_create_signed = tx_create.sign([b.me_private]) + block = b.create_block([tx_create_signed]) b.write_block(block, durability='hard') # get input owned_inputs_user1 = b.get_owned_ids(user_vk) owned_inputs_user2 = b.get_owned_ids(user2_vk) - expected_owned_inputs_user1 = [TransactionLink(tx.id, 0) for tx - in transactions] + expected_owned_inputs_user1 = [TransactionLink(tx_create.id, 0), + TransactionLink(tx_create.id, 1)] assert owned_inputs_user1 == expected_owned_inputs_user1 assert owned_inputs_user2 == [] - inputs = sum([tx.to_inputs() for tx in transactions], []) - tx = Transaction.transfer(inputs, len(inputs) * [[user2_vk]]) - tx = tx.sign([user_sk]) - block = b.create_block([tx]) + # transfer divisible asset divided in two outputs + tx_transfer = Transaction.transfer(tx_create.to_inputs(), + [([user2_vk], 1), ([user2_vk], 1)], + asset=tx_create.asset) + tx_transfer_signed = tx_transfer.sign([user_sk]) + block = b.create_block([tx_transfer_signed]) b.write_block(block, durability='hard') owned_inputs_user1 = b.get_owned_ids(user_vk) owned_inputs_user2 = b.get_owned_ids(user2_vk) assert owned_inputs_user1 == [] - assert owned_inputs_user2 == [TransactionLink(tx.id, 0), - TransactionLink(tx.id, 1)] + assert owned_inputs_user2 == [TransactionLink(tx_transfer.id, 0), + TransactionLink(tx_transfer.id, 1)] def test_get_owned_ids_multiple_owners(self, b, user_sk, user_vk): from bigchaindb.common import crypto @@ -1122,7 +1032,7 @@ class TestMultipleInputs(object): user2_sk, user2_vk = crypto.generate_key_pair() user3_sk, user3_vk = crypto.generate_key_pair() - tx = Transaction.create([b.me], [user_vk, user2_vk]) + tx = Transaction.create([b.me], [([user_vk, user2_vk], 1)]) tx = tx.sign([b.me_private]) block = b.create_block([tx]) b.write_block(block, durability='hard') @@ -1134,7 +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_vk], tx.asset) + tx = Transaction.transfer(tx.to_inputs(), [([user3_vk], 1)], tx.asset) tx = tx.sign([user_sk, user2_sk]) block = b.create_block([tx]) b.write_block(block, durability='hard') @@ -1150,7 +1060,7 @@ class TestMultipleInputs(object): user2_sk, user2_vk = crypto.generate_key_pair() - tx = Transaction.create([b.me], [user_vk]) + tx = Transaction.create([b.me], [([user_vk], 1)]) tx = tx.sign([b.me_private]) block = b.create_block([tx]) b.write_block(block, durability='hard') @@ -1164,7 +1074,7 @@ class TestMultipleInputs(object): assert spent_inputs_user1 is None # create a transaction and block - tx = Transaction.transfer(tx.to_inputs(), [user2_vk], tx.asset) + tx = Transaction.transfer(tx.to_inputs(), [([user2_vk], 1)], tx.asset) tx = tx.sign([user_sk]) block = b.create_block([tx]) b.write_block(block, durability='hard') @@ -1181,7 +1091,7 @@ class TestMultipleInputs(object): # create a new users user2_sk, user2_vk = crypto.generate_key_pair() - tx = Transaction.create([b.me], [user_vk]) + tx = Transaction.create([b.me], [([user_vk], 1)]) tx = tx.sign([b.me_private]) block = b.create_block([tx]) b.write_block(block, durability='hard') @@ -1199,7 +1109,7 @@ class TestMultipleInputs(object): assert spent_inputs_user1 is None # create a transaction and block - tx = Transaction.transfer(tx.to_inputs(), [user2_vk], tx.asset) + tx = Transaction.transfer(tx.to_inputs(), [([user2_vk], 1)], tx.asset) tx = tx.sign([user_sk]) block = b.create_block([tx]) b.write_block(block, durability='hard') @@ -1214,24 +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_vk): - import random from bigchaindb.common import crypto from bigchaindb.models import Transaction + from bigchaindb.common.transaction import Asset # create a new users user2_sk, user2_vk = crypto.generate_key_pair() - transactions = [] - for i in range(3): - payload = {'somedata': random.randint(0, 255)} - tx = Transaction.create([b.me], [user_vk], payload) - tx = tx.sign([b.me_private]) - transactions.append(tx) - block = b.create_block(transactions) + # create a divisible asset with 3 outputs + asset = Asset(divisible=True) + tx_create = Transaction.create([b.me], + [([user_vk], 1), + ([user_vk], 1), + ([user_vk], 1)], + asset=asset) + tx_create_signed = tx_create.sign([b.me_private]) + block = b.create_block([tx_create_signed]) b.write_block(block, durability='hard') owned_inputs_user1 = b.get_owned_ids(user_vk) @@ -1240,22 +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_vk]]) - tx = tx.sign([user_sk]) - block = b.create_block([tx]) + # transfer the first 2 inputs + tx_transfer = Transaction.transfer(tx_create.to_inputs()[:2], + [([user2_vk], 1), ([user2_vk], 1)], + asset=tx_create.asset) + tx_transfer_signed = tx_transfer.sign([user_sk]) + block = b.create_block([tx_transfer_signed]) b.write_block(block, durability='hard') # check that used inputs are marked as spent - for ffill in inputs: - assert b.get_spent(ffill.tx_input.txid, ffill.tx_input.cid) == tx + for ffill in tx_create.to_inputs()[:2]: + spent_tx = b.get_spent(ffill.tx_input.txid, ffill.tx_input.cid) + assert spent_tx == tx_transfer_signed # check if remaining transaction that was unspent is also perceived # spendable by BigchainDB - assert b.get_spent(transactions[2].id, 0) is None + assert b.get_spent(tx_create.to_inputs()[2].tx_input.txid, 2) is None def test_get_spent_multiple_owners(self, b, user_sk, user_vk): import random @@ -1268,7 +1177,8 @@ class TestMultipleInputs(object): transactions = [] for i in range(3): payload = {'somedata': random.randint(0, 255)} - tx = Transaction.create([b.me], [user_vk, user2_vk], payload) + tx = Transaction.create([b.me], [([user_vk, user2_vk], 1)], + payload) tx = tx.sign([b.me_private]) transactions.append(tx) block = b.create_block(transactions) @@ -1281,7 +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_vk], transactions[0].asset) + tx = Transaction.transfer(transactions[0].to_inputs(), + [([user3_vk], 1)], transactions[0].asset) tx = tx.sign([user_sk, user2_sk]) block = b.create_block([tx]) b.write_block(block, durability='hard') diff --git a/tests/pipelines/test_block_creation.py b/tests/pipelines/test_block_creation.py index c2403a08..171e3ca9 100644 --- a/tests/pipelines/test_block_creation.py +++ b/tests/pipelines/test_block_creation.py @@ -45,7 +45,7 @@ def test_create_block(b, user_vk): block_maker = BlockPipeline() for i in range(100): - tx = Transaction.create([b.me], [user_vk]) + tx = Transaction.create([b.me], [([user_vk], 1)]) tx = tx.sign([b.me_private]) block_maker.create(tx) @@ -63,7 +63,7 @@ def test_write_block(b, user_vk): txs = [] for i in range(100): - tx = Transaction.create([b.me], [user_vk]) + tx = Transaction.create([b.me], [([user_vk], 1)]) tx = tx.sign([b.me_private]) txs.append(tx) @@ -82,7 +82,7 @@ def test_duplicate_transaction(b, user_vk): txs = [] for i in range(10): - tx = Transaction.create([b.me], [user_vk]) + tx = Transaction.create([b.me], [([user_vk], 1)]) tx = tx.sign([b.me_private]) txs.append(tx) @@ -97,8 +97,7 @@ def test_duplicate_transaction(b, user_vk): # verify tx is in the backlog assert b.connection.run(r.table('backlog').get(txs[0].id)) is not None - # try to validate a transaction that's already in the chain; should not - # work + # try to validate a transaction that's already in the chain; should not work assert block_maker.validate_tx(txs[0].to_dict()) is None # duplicate tx should be removed from backlog @@ -110,7 +109,7 @@ def test_delete_tx(b, user_vk): from bigchaindb.pipelines.block import BlockPipeline block_maker = BlockPipeline() for i in range(100): - tx = Transaction.create([b.me], [user_vk]) + tx = Transaction.create([b.me], [([user_vk], 1)]) tx = tx.sign([b.me_private]) block_maker.create(tx) # make sure the tx appears in the backlog @@ -139,7 +138,8 @@ def test_prefeed(b, user_vk): from bigchaindb.pipelines.block import initial for i in range(100): - tx = Transaction.create([b.me], [user_vk], {'msg': random.random()}) + tx = Transaction.create([b.me], [([user_vk], 1)], + {'msg': random.random()}) tx = tx.sign([b.me_private]) b.write_transaction(tx) @@ -168,7 +168,8 @@ def test_full_pipeline(b, user_vk): count_assigned_to_me = 0 for i in range(100): - tx = Transaction.create([b.me], [user_vk], {'msg': random.random()}) + tx = Transaction.create([b.me], [([user_vk], 1)], + {'msg': random.random()}) tx = tx.sign([b.me_private]).to_dict() assignee = random.choice([b.me, 'aaa', 'bbb', 'ccc']) tx['assignee'] = assignee diff --git a/tests/pipelines/test_election.py b/tests/pipelines/test_election.py index 669a75cb..d62869f5 100644 --- a/tests/pipelines/test_election.py +++ b/tests/pipelines/test_election.py @@ -15,7 +15,7 @@ def test_check_for_quorum_invalid(b, user_vk): e = election.Election() # create blocks with transactions - tx1 = Transaction.create([b.me], [user_vk]) + tx1 = Transaction.create([b.me], [([user_vk], 1)]) test_block = b.create_block([tx1]) # simulate a federation with four voters @@ -44,7 +44,7 @@ def test_check_for_quorum_invalid_prev_node(b, user_vk): e = election.Election() # create blocks with transactions - tx1 = Transaction.create([b.me], [user_vk]) + tx1 = Transaction.create([b.me], [([user_vk], 1)]) test_block = b.create_block([tx1]) # simulate a federation with four voters @@ -74,7 +74,7 @@ def test_check_for_quorum_valid(b, user_vk): e = election.Election() # create blocks with transactions - tx1 = Transaction.create([b.me], [user_vk]) + tx1 = Transaction.create([b.me], [([user_vk], 1)]) test_block = b.create_block([tx1]) # simulate a federation with four voters @@ -103,7 +103,7 @@ def test_check_requeue_transaction(b, user_vk): e = election.Election() # create blocks with transactions - tx1 = Transaction.create([b.me], [user_vk]) + tx1 = Transaction.create([b.me], [([user_vk], 1)]) test_block = b.create_block([tx1]) e.requeue_transactions(test_block) @@ -131,7 +131,8 @@ def test_full_pipeline(b, user_vk): # write two blocks txs = [] for i in range(100): - tx = Transaction.create([b.me], [user_vk], {'msg': random.random()}) + tx = Transaction.create([b.me], [([user_vk], 1)], + {'msg': random.random()}) tx = tx.sign([b.me_private]) txs.append(tx) @@ -140,7 +141,8 @@ def test_full_pipeline(b, user_vk): txs = [] for i in range(100): - tx = Transaction.create([b.me], [user_vk], {'msg': random.random()}) + tx = Transaction.create([b.me], [([user_vk], 1)], + {'msg': random.random()}) tx = tx.sign([b.me_private]) txs.append(tx) diff --git a/tests/pipelines/test_stale_monitor.py b/tests/pipelines/test_stale_monitor.py index 3a3e6ffe..95f298c5 100644 --- a/tests/pipelines/test_stale_monitor.py +++ b/tests/pipelines/test_stale_monitor.py @@ -10,7 +10,7 @@ import os def test_get_stale(b, user_vk): from bigchaindb.models import Transaction - tx = Transaction.create([b.me], [user_vk]) + tx = Transaction.create([b.me], [([user_vk], 1)]) tx = tx.sign([b.me_private]) b.write_transaction(tx, durability='hard') @@ -27,7 +27,7 @@ def test_get_stale(b, user_vk): def test_reassign_transactions(b, user_vk): from bigchaindb.models import Transaction # test with single node - tx = Transaction.create([b.me], [user_vk]) + tx = Transaction.create([b.me], [([user_vk], 1)]) tx = tx.sign([b.me_private]) b.write_transaction(tx, durability='hard') @@ -36,7 +36,7 @@ def test_reassign_transactions(b, user_vk): stm.reassign_transactions(tx.to_dict()) # test with federation - tx = Transaction.create([b.me], [user_vk]) + tx = Transaction.create([b.me], [([user_vk], 1)]) tx = tx.sign([b.me_private]) b.write_transaction(tx, durability='hard') @@ -51,7 +51,7 @@ def test_reassign_transactions(b, user_vk): assert reassigned_tx['assignee'] != tx['assignee'] # test with node not in federation - tx = Transaction.create([b.me], [user_vk]) + tx = Transaction.create([b.me], [([user_vk], 1)]) tx = tx.sign([b.me_private]).to_dict() tx.update({'assignee': 'lol'}) tx.update({'assignment_timestamp': time.time()}) @@ -85,7 +85,7 @@ def test_full_pipeline(monkeypatch, user_vk): monkeypatch.setattr('time.time', lambda: 1) for i in range(100): - tx = Transaction.create([b.me], [user_vk]) + tx = Transaction.create([b.me], [([user_vk], 1)]) tx = tx.sign([b.me_private]) original_txc.append(tx.to_dict()) diff --git a/tests/pipelines/test_vote.py b/tests/pipelines/test_vote.py index 5bd0eb52..ec2f6204 100644 --- a/tests/pipelines/test_vote.py +++ b/tests/pipelines/test_vote.py @@ -8,7 +8,7 @@ from multipipes import Pipe, Pipeline def dummy_tx(b): from bigchaindb.models import Transaction - tx = Transaction.create([b.me], [b.me]) + tx = Transaction.create([b.me], [([b.me], 1)]) tx = tx.sign([b.me_private]) return tx @@ -130,7 +130,7 @@ def test_vote_validate_transaction(b): assert validation == (True, 123, 1) # NOTE: Submit unsigned transaction to `validate_tx` yields `False`. - tx = Transaction.create([b.me], [b.me]) + tx = Transaction.create([b.me], [([b.me], 1)]) validation = vote_obj.validate_tx(tx, 456, 10) assert validation == (False, 456, 10) @@ -224,7 +224,7 @@ def test_valid_block_voting_with_create_transaction(b, monkeypatch): # create a `CREATE` transaction test_user_priv, test_user_pub = crypto.generate_key_pair() - tx = Transaction.create([b.me], [test_user_pub]) + tx = Transaction.create([b.me], [([test_user_pub], 1)]) tx = tx.sign([b.me_private]) monkeypatch.setattr('time.time', lambda: 1) @@ -265,7 +265,7 @@ def test_valid_block_voting_with_transfer_transactions(monkeypatch, b): # create a `CREATE` transaction test_user_priv, test_user_pub = crypto.generate_key_pair() - tx = Transaction.create([b.me], [test_user_pub]) + tx = Transaction.create([b.me], [([test_user_pub], 1)]) tx = tx.sign([b.me_private]) monkeypatch.setattr('time.time', lambda: 1) @@ -274,7 +274,8 @@ def test_valid_block_voting_with_transfer_transactions(monkeypatch, b): # create a `TRANSFER` transaction test_user2_priv, test_user2_pub = crypto.generate_key_pair() - tx2 = Transaction.transfer(tx.to_inputs(), [test_user2_pub], tx.asset) + tx2 = Transaction.transfer(tx.to_inputs(), [([test_user2_pub], 1)], + tx.asset) tx2 = tx2.sign([test_user_priv]) monkeypatch.setattr('time.time', lambda: 2) @@ -338,7 +339,7 @@ def test_unsigned_tx_in_block_voting(monkeypatch, b, user_vk): vote_pipeline.setup(indata=inpipe, outdata=outpipe) # NOTE: `tx` is invalid, because it wasn't signed. - tx = Transaction.create([b.me], [b.me]) + tx = Transaction.create([b.me], [([b.me], 1)]) block = b.create_block([tx]) inpipe.put(block.to_dict()) @@ -375,7 +376,7 @@ def test_invalid_id_tx_in_block_voting(monkeypatch, b, user_vk): vote_pipeline.setup(indata=inpipe, outdata=outpipe) # NOTE: `tx` is invalid, because its id is not corresponding to its content - tx = Transaction.create([b.me], [b.me]) + tx = Transaction.create([b.me], [([b.me], 1)]) tx = tx.sign([b.me_private]) block = b.create_block([tx]).to_dict() block['block']['transactions'][0]['id'] = 'an invalid tx id' @@ -414,7 +415,7 @@ def test_invalid_content_in_tx_in_block_voting(monkeypatch, b, user_vk): vote_pipeline.setup(indata=inpipe, outdata=outpipe) # NOTE: `tx` is invalid, because its content is not corresponding to its id - tx = Transaction.create([b.me], [b.me]) + tx = Transaction.create([b.me], [([b.me], 1)]) tx = tx.sign([b.me_private]) block = b.create_block([tx]).to_dict() block['block']['transactions'][0]['id'] = 'an invalid tx id' diff --git a/tests/test_core.py b/tests/test_core.py index 84cdba08..55d73e77 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -62,14 +62,13 @@ def test_bigchain_class_initialization_with_parameters(config): def test_get_blocks_status_containing_tx(monkeypatch): + from bigchaindb.db.backends.rethinkdb import RethinkDBBackend from bigchaindb.core import Bigchain blocks = [ {'id': 1}, {'id': 2} ] - monkeypatch.setattr( - Bigchain, 'search_block_election_on_index', lambda x, y: blocks) - monkeypatch.setattr( - Bigchain, 'block_election_status', lambda x, y, z: Bigchain.BLOCK_VALID) + monkeypatch.setattr(RethinkDBBackend, 'get_blocks_status_from_transaction', lambda x: blocks) + monkeypatch.setattr(Bigchain, 'block_election_status', lambda x, y, z: Bigchain.BLOCK_VALID) bigchain = Bigchain(public_key='pubkey', private_key='privkey') with pytest.raises(Exception): bigchain.get_blocks_status_containing_tx('txid') @@ -85,10 +84,9 @@ def test_has_previous_vote(monkeypatch): bigchain.has_previous_vote(block) -@pytest.mark.parametrize('items,exists', (((0,), True), ((), False))) -def test_transaction_exists(monkeypatch, items, exists): +@pytest.mark.parametrize('count,exists', ((1, True), (0, False))) +def test_transaction_exists(monkeypatch, count, exists): from bigchaindb.core import Bigchain - monkeypatch.setattr( - RqlQuery, 'run', lambda x, y: namedtuple('response', 'items')(items)) + monkeypatch.setattr(RqlQuery, 'run', lambda x, y: count) bigchain = Bigchain(public_key='pubkey', private_key='privkey') assert bigchain.transaction_exists('txid') is exists diff --git a/tests/test_models.py b/tests/test_models.py index 5033aebb..534052f9 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -5,7 +5,7 @@ class TestTransactionModel(object): def test_validating_an_invalid_transaction(self, b): from bigchaindb.models import Transaction - tx = Transaction.create([b.me], [b.me]) + tx = Transaction.create([b.me], [([b.me], 1)]) tx.operation = 'something invalid' with raises(TypeError): @@ -41,7 +41,7 @@ class TestBlockModel(object): from bigchaindb.common.util import gen_timestamp, serialize from bigchaindb.models import Block, Transaction - transactions = [Transaction.create([b.me], [b.me])] + transactions = [Transaction.create([b.me], [([b.me], 1)])] timestamp = gen_timestamp() voters = ['Qaaa', 'Qbbb'] expected_block = { @@ -73,7 +73,7 @@ class TestBlockModel(object): from bigchaindb.common.util import gen_timestamp, serialize from bigchaindb.models import Block, Transaction - transactions = [Transaction.create([b.me], [b.me])] + transactions = [Transaction.create([b.me], [([b.me], 1)])] timestamp = gen_timestamp() voters = ['Qaaa', 'Qbbb'] expected = Block(transactions, b.me, timestamp, voters) @@ -113,7 +113,7 @@ class TestBlockModel(object): from bigchaindb.common.util import gen_timestamp, serialize from bigchaindb.models import Block, Transaction - transactions = [Transaction.create([b.me], [b.me])] + transactions = [Transaction.create([b.me], [([b.me], 1)])] timestamp = gen_timestamp() voters = ['Qaaa', 'Qbbb'] @@ -136,7 +136,7 @@ class TestBlockModel(object): def test_compare_blocks(self, b): from bigchaindb.models import Block, Transaction - transactions = [Transaction.create([b.me], [b.me])] + transactions = [Transaction.create([b.me], [([b.me], 1)])] assert Block() != 'invalid comparison' assert Block(transactions) == Block(transactions) @@ -146,7 +146,7 @@ class TestBlockModel(object): from bigchaindb.common.util import gen_timestamp, serialize from bigchaindb.models import Block, Transaction - transactions = [Transaction.create([b.me], [b.me])] + transactions = [Transaction.create([b.me], [([b.me], 1)])] timestamp = gen_timestamp() voters = ['Qaaa', 'Qbbb'] expected_block = { @@ -168,7 +168,7 @@ class TestBlockModel(object): from unittest.mock import Mock from bigchaindb.models import Transaction - tx = Transaction.create([b.me], [b.me]) + tx = Transaction.create([b.me], [([b.me], 1)]) block = b.create_block([tx]) has_previous_vote = Mock() diff --git a/tests/web/test_basic_views.py b/tests/web/test_basic_views.py index 00e40a37..7d382ca5 100644 --- a/tests/web/test_basic_views.py +++ b/tests/web/test_basic_views.py @@ -36,7 +36,7 @@ def test_post_create_transaction_endpoint(b, client): from bigchaindb.models import Transaction user_priv, user_pub = crypto.generate_key_pair() - tx = Transaction.create([user_pub], [user_pub]) + tx = Transaction.create([user_pub], [([user_pub], 1)]) tx = tx.sign([user_priv]) res = client.post(TX_ENDPOINT, data=json.dumps(tx.to_dict())) @@ -48,7 +48,7 @@ def test_post_create_transaction_with_invalid_id(b, client): from bigchaindb.models import Transaction user_priv, user_pub = crypto.generate_key_pair() - tx = Transaction.create([user_pub], [user_pub]) + tx = Transaction.create([user_pub], [([user_pub], 1)]) tx = tx.sign([user_priv]).to_dict() tx['id'] = 'invalid id' @@ -60,7 +60,7 @@ def test_post_create_transaction_with_invalid_signature(b, client): from bigchaindb.models import Transaction user_priv, user_pub = crypto.generate_key_pair() - tx = Transaction.create([user_pub], [user_pub]) + tx = Transaction.create([user_pub], [([user_pub], 1)]) tx = tx.sign([user_priv]).to_dict() tx['transaction']['fulfillments'][0]['fulfillment'] = 'invalid signature' @@ -77,7 +77,8 @@ def test_post_transfer_transaction_endpoint(b, client, user_vk, user_sk): input_valid = b.get_owned_ids(user_vk).pop() create_tx = b.get_transaction(input_valid.txid) - transfer_tx = Transaction.transfer(create_tx.to_inputs(), [user_pub], create_tx.asset) + transfer_tx = Transaction.transfer(create_tx.to_inputs(), + [([user_pub], 1)], create_tx.asset) transfer_tx = transfer_tx.sign([user_sk]) res = client.post(TX_ENDPOINT, data=json.dumps(transfer_tx.to_dict())) @@ -94,7 +95,8 @@ def test_post_invalid_transfer_transaction_returns_400(b, client, user_vk, user_ input_valid = b.get_owned_ids(user_vk).pop() create_tx = b.get_transaction(input_valid.txid) - transfer_tx = Transaction.transfer(create_tx.to_inputs(), [user_pub], create_tx.asset) + transfer_tx = Transaction.transfer(create_tx.to_inputs(), + [([user_pub], 1)], create_tx.asset) res = client.post(TX_ENDPOINT, data=json.dumps(transfer_tx.to_dict())) assert res.status_code == 400