From 50b0b3cef217bc412dba5a062de1db2d1adcd514 Mon Sep 17 00:00:00 2001 From: Sylvain Bellemare Date: Thu, 29 Sep 2016 10:29:41 +0200 Subject: [PATCH] Rebase/feat/586/integrate tx model (#641) * Adjust imports to bigchaindb_common * Adjust get_spent function signature * Adjust block serialization * Fix BigchainApi Test * Fix TestTransactionValidation tests * Fix TestBlockValidation tests * WIP: TestMultipleInputs * Adjust tests to tx-model interface changes - Fix old tests - Fix tests in TestMultipleInputs class * Remove fulfillment message tests * Fix TransactionMalleability tests * Remove Cryptoconditions tests * Remove create_transaction * Remove signing logic * Remove consensus plugin * Fix block_creation pipeline * Fix election pipeline * Replace some util functions with bdb_common ones - timestamp ==> gen_timestamp - serialize. * Implement Block model * Simplify function signatures for vote functions Change parameter interface for the following functions: - has_previous_vote - verify_vote_signature - block_election_status so that they take a block's id and voters instead of a fake block. * Integrate Block and Transaction model * Fix leftover tests and cleanup conftest * Add bigchaindb-common to install_requires * Delete transactions after block is written (#609) * delete transactions after block is written * cleanup transaction_exists * check for duplicate transactions * delete invalid tx from backlog * test duplicate transaction * Remove dead code * Test processes.py * Test invalid tx in on server * Fix tests for core.py * Fix models tests * Test commands main fn * Add final coverage to vote pipeline * Add more tests to voting pipeline * Remove consensus plugin docs and misc * Post rebase fixes * Fix rebase mess * Remove extra blank line * Improve docstring * Remove comment handled in bigchaindb/cryptoconditions#27; see https://github.com/bigchaindb/cryptoconditions/issues/27 * Fix block serialization in block creation * Add signed_ prefix to transfer_tx * Improve docs * Add library documentation page on pipelines * PR feedback for models.py * Impr. readability of get_last_voted_block * Use dict comprehension * Add docker-compose file to build and serve docs locally for development purposes * Change private_key for signing_key * Improve docstrings * Remove consensus docs * Document new consensus module * Create different transactions for the block * Cleanup variable names in block.py * Create different transactions for the block * Cleanup variable names in block.py --- benchmarking-tests/benchmark_utils.py | 8 +- bigchaindb/__init__.py | 1 - bigchaindb/client.py | 115 - bigchaindb/commands/bigchain.py | 16 +- bigchaindb/commands/utils.py | 5 +- bigchaindb/config_utils.py | 43 +- bigchaindb/consensus.py | 238 +- bigchaindb/core.py | 327 +-- bigchaindb/crypto.py | 17 - bigchaindb/db/utils.py | 2 +- bigchaindb/exceptions.py | 65 - bigchaindb/models.py | 208 ++ bigchaindb/monitor.py | 1 - bigchaindb/pipelines/block.py | 64 +- bigchaindb/pipelines/election.py | 18 +- bigchaindb/pipelines/vote.py | 64 +- bigchaindb/util.py | 491 +--- bigchaindb/web/server.py | 1 - bigchaindb/web/views/info.py | 1 - bigchaindb/web/views/transactions.py | 24 +- deploy-cluster-aws/write_keypairs_file.py | 4 +- docs.yml | 16 + docs/source/appendices/consensus.md | 82 - docs/source/appendices/consensus.rst | 6 + docs/source/appendices/index.rst | 5 +- docs/source/appendices/pipelines.rst | 37 + docs/source/server-reference/configuration.md | 16 - setup.py | 4 +- speed-tests/speed_tests.py | 2 + tests/conftest.py | 18 + tests/db/conftest.py | 20 +- tests/db/test_bigchain_api.py | 2209 +++++------------ tests/db/test_utils.py | 5 +- .../doc/run_doc_python_server_api_examples.py | 9 +- tests/pipelines/conftest.py | 1 - tests/pipelines/test_block_creation.py | 135 +- tests/pipelines/test_election.py | 71 +- tests/pipelines/test_stale_monitor.py | 34 +- tests/pipelines/test_vote.py | 278 ++- tests/test_client.py | 74 - tests/test_commands.py | 92 +- tests/test_config_utils.py | 38 +- tests/test_consensus.py | 15 +- tests/test_core.py | 9 +- tests/test_models.py | 178 ++ tests/test_util.py | 79 - tests/web/conftest.py | 4 +- tests/web/test_basic_views.py | 83 +- tests/web/test_server.py | 4 - 49 files changed, 1921 insertions(+), 3316 deletions(-) delete mode 100644 bigchaindb/client.py delete mode 100644 bigchaindb/crypto.py delete mode 100644 bigchaindb/exceptions.py create mode 100644 bigchaindb/models.py create mode 100644 docs.yml delete mode 100644 docs/source/appendices/consensus.md create mode 100644 docs/source/appendices/consensus.rst create mode 100644 docs/source/appendices/pipelines.rst delete mode 100644 tests/test_client.py create mode 100644 tests/test_models.py diff --git a/benchmarking-tests/benchmark_utils.py b/benchmarking-tests/benchmark_utils.py index 41b0e88e..86242753 100644 --- a/benchmarking-tests/benchmark_utils.py +++ b/benchmarking-tests/benchmark_utils.py @@ -8,6 +8,7 @@ import logging import rethinkdb as r from os.path import expanduser +from bigchaindb_common.transaction import Transaction from bigchaindb import Bigchain from bigchaindb.util import ProcessGroup @@ -33,10 +34,9 @@ def create_write_transaction(tx_left, payload_filler): # Include a random uuid string in the payload to prevent duplicate # transactions (i.e. transactions with the same hash) payload_dict['msg'] = str(uuid.uuid4()) - tx = b.create_transaction(b.me, b.me, None, 'CREATE', - payload=payload_dict) - tx_signed = b.sign_transaction(tx, b.me_private) - b.write_transaction(tx_signed) + tx = Transaction.create([b.me], [b.me], payload=payload_dict) + tx = tx.sign([b.me_private]) + b.write_transaction(tx) tx_left -= 1 diff --git a/bigchaindb/__init__.py b/bigchaindb/__init__.py index d5fe15e6..a89692aa 100644 --- a/bigchaindb/__init__.py +++ b/bigchaindb/__init__.py @@ -30,7 +30,6 @@ config = { 'rate': 0.01, }, 'api_endpoint': 'http://localhost:9984/api/v1', - 'consensus_plugin': 'default', 'backlog_reassign_delay': 30 } diff --git a/bigchaindb/client.py b/bigchaindb/client.py deleted file mode 100644 index 9f12c8e4..00000000 --- a/bigchaindb/client.py +++ /dev/null @@ -1,115 +0,0 @@ -import requests - -import bigchaindb -from bigchaindb import config_utils -from bigchaindb import exceptions -from bigchaindb import crypto - - -class Client: - """Client for BigchainDB. - - A Client is initialized with a keypair and is able to create, sign, and submit transactions to a Node - in the Federation. At the moment, a Client instance is bounded to a specific ``host`` in the Federation. - In the future, a Client might connect to >1 hosts. - """ - - def __init__(self, public_key=None, private_key=None, api_endpoint=None, - consensus_plugin=None): - """Initialize the Client instance - - There are three ways in which the Client instance can get its parameters. - The order by which the parameters are chosen are: - - 1. Setting them by passing them to the `__init__` method itself. - 2. Setting them as environment variables - 3. Reading them from the `config.json` file. - - Args: - public_key (str): the base58 encoded public key for the ED25519 curve. - private_key (str): the base58 encoded private key for the ED25519 curve. - api_endpoint (str): a URL where rethinkdb is running. - format: scheme://hostname:port - consensus_plugin (str): the registered name of your installed - consensus plugin. The `core` plugin is built into BigchainDB; - others must be installed via pip. - """ - - config_utils.autoconfigure() - - self.public_key = public_key or bigchaindb.config['keypair']['public'] - self.private_key = private_key or bigchaindb.config['keypair']['private'] - self.api_endpoint = api_endpoint or bigchaindb.config['api_endpoint'] - self.consensus = config_utils.load_consensus_plugin(consensus_plugin) - - if not self.public_key or not self.private_key: - raise exceptions.KeypairNotFoundException() - - def create(self, payload=None): - """Issue a transaction to create an asset. - - Args: - payload (dict): the payload for the transaction. - - Return: - The transaction pushed to the Federation. - """ - - tx = self.consensus.create_transaction( - owner_before=self.public_key, - owner_after=self.public_key, - tx_input=None, - operation='CREATE', - payload=payload) - - signed_tx = self.consensus.sign_transaction( - tx, private_key=self.private_key) - return self._push(signed_tx) - - def transfer(self, owner_after, tx_input, payload=None): - """Issue a transaction to transfer an asset. - - Args: - owner_after (str): the public key of the new owner - tx_input (str): the id of the transaction to use as input - payload (dict, optional): the payload for the transaction. - - Return: - The transaction pushed to the Federation. - """ - - tx = self.consensus.create_transaction( - owner_before=self.public_key, - owner_after=owner_after, - tx_input=tx_input, - operation='TRANSFER', - payload=payload) - - signed_tx = self.consensus.sign_transaction( - tx, private_key=self.private_key) - return self._push(signed_tx) - - def _push(self, tx): - """Submit a transaction to the Federation. - - Args: - tx (dict): the transaction to be pushed to the Federation. - - Return: - The transaction pushed to the Federation. - """ - - res = requests.post(self.api_endpoint + '/transactions/', json=tx) - return res.json() - - -def temp_client(): - """Create a new temporary client. - - Return: - A client initialized with a keypair generated on the fly. - """ - - private_key, public_key = crypto.generate_key_pair() - return Client(private_key=private_key, public_key=public_key, api_endpoint=bigchaindb.config['api_endpoint']) - diff --git a/bigchaindb/commands/bigchain.py b/bigchaindb/commands/bigchain.py index 46622867..b5c0e847 100644 --- a/bigchaindb/commands/bigchain.py +++ b/bigchaindb/commands/bigchain.py @@ -13,19 +13,19 @@ import builtins import logstats +from bigchaindb_common import crypto +from bigchaindb_common.exceptions import (StartupError, + DatabaseAlreadyExists, + KeypairNotFoundException) import rethinkdb as r import bigchaindb import bigchaindb.config_utils +from bigchaindb.models import Transaction from bigchaindb.util import ProcessGroup -from bigchaindb.client import temp_client from bigchaindb import db -from bigchaindb.exceptions import (StartupError, - DatabaseAlreadyExists, - KeypairNotFoundException) from bigchaindb.commands import utils from bigchaindb import processes -from bigchaindb import crypto logging.basicConfig(level=logging.INFO) @@ -193,10 +193,12 @@ def run_start(args): def _run_load(tx_left, stats): logstats.thread.start(stats) - client = temp_client() + b = bigchaindb.Bigchain() while True: - tx = client.create() + tx = Transaction.create([b.me], [b.me]) + tx = tx.sign([b.me_private]) + b.write_transaction(tx) stats['transactions'] += 1 diff --git a/bigchaindb/commands/utils.py b/bigchaindb/commands/utils.py index 36b78653..1391f18f 100644 --- a/bigchaindb/commands/utils.py +++ b/bigchaindb/commands/utils.py @@ -3,17 +3,18 @@ for ``argparse.ArgumentParser``. """ import argparse +from bigchaindb_common.exceptions import StartupError import multiprocessing as mp import subprocess import rethinkdb as r import bigchaindb -from bigchaindb.exceptions import StartupError 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. @@ -24,7 +25,7 @@ def start_rethinkdb(): starting the db Raises: - ``bigchaindb.exceptions.StartupError`` if RethinkDB cannot + ``bigchaindb_common.exceptions.StartupError`` if RethinkDB cannot be started. """ diff --git a/bigchaindb/config_utils.py b/bigchaindb/config_utils.py index e1dcbdc9..7168806d 100644 --- a/bigchaindb/config_utils.py +++ b/bigchaindb/config_utils.py @@ -16,13 +16,11 @@ import copy import json import logging import collections -from functools import lru_cache -from pkg_resources import iter_entry_points, ResolutionError +from bigchaindb_common import exceptions import bigchaindb -from bigchaindb.consensus import AbstractConsensusRules -from bigchaindb import exceptions + # TODO: move this to a proper configuration file for logging logging.getLogger('requests').setLevel(logging.WARNING) @@ -242,40 +240,3 @@ def autoconfigure(filename=None, config=None, force=False): newconfig = update(newconfig, config) set_config(newconfig) # sets bigchaindb.config - - -@lru_cache() -def load_consensus_plugin(name=None): - """Find and load the chosen consensus plugin. - - Args: - name (string): the name of the entry_point, as advertised in the - setup.py of the providing package. - - Returns: - an uninstantiated subclass of ``bigchaindb.consensus.AbstractConsensusRules`` - """ - if not name: - name = bigchaindb.config.get('consensus_plugin', 'default') - - # TODO: This will return the first plugin with group `bigchaindb.consensus` - # and name `name` in the active WorkingSet. - # We should probably support Requirements specs in the config, e.g. - # consensus_plugin: 'my-plugin-package==0.0.1;default' - plugin = None - for entry_point in iter_entry_points('bigchaindb.consensus', name): - plugin = entry_point.load() - - # No matching entry_point found - if not plugin: - raise ResolutionError( - 'No plugin found in group `bigchaindb.consensus` with name `{}`'. - format(name)) - - # Is this strictness desireable? - # It will probably reduce developer headaches in the wild. - if not issubclass(plugin, (AbstractConsensusRules)): - raise TypeError("object of type '{}' does not implement `bigchaindb." - "consensus.AbstractConsensusRules`".format(type(plugin))) - - return plugin diff --git a/bigchaindb/consensus.py b/bigchaindb/consensus.py index d799018f..bfb054af 100644 --- a/bigchaindb/consensus.py +++ b/bigchaindb/consensus.py @@ -1,244 +1,28 @@ -from abc import ABCMeta, abstractmethod - -from bigchaindb import crypto, exceptions, util +from bigchaindb.util import verify_vote_signature -class AbstractConsensusRules(metaclass=ABCMeta): - """Abstract base class for Bigchain plugins which implement consensus logic. - - A consensus plugin must expose a class inheriting from this one via an - entry_point. - - All methods listed below must be implemented. - """ - - @staticmethod - @abstractmethod - def validate_transaction(bigchain, transaction): - """Validate a transaction. - - Args: - bigchain (Bigchain): an instantiated ``bigchaindb.Bigchain`` object. - transaction (dict): transaction to validate. - - Returns: - The transaction if the transaction is valid else it raises an - exception describing the reason why the transaction is invalid. - - Raises: - Descriptive exceptions indicating the reason the transaction failed. - See the `exceptions` module for bigchain-native error classes. - """ - - @staticmethod - @abstractmethod - def validate_block(bigchain, block): - """Validate a block. - - Args: - bigchain (Bigchain): an instantiated ``bigchaindb.Bigchain`` object. - block (dict): block to validate. - - Returns: - The block if the block is valid else it raises an exception - describing the reason why the block is invalid. - - Raises: - Descriptive exceptions indicating the reason the block failed. - See the `exceptions` module for bigchain-native error classes. - """ - - @staticmethod - @abstractmethod - def create_transaction(*args, **kwargs): - """Create a new transaction. - - Args: - The signature of this method is left to plugin authors to decide. - - Returns: - dict: newly constructed transaction. - """ - - @staticmethod - @abstractmethod - def sign_transaction(transaction, *args, **kwargs): - """Sign a transaction. - - Args: - transaction (dict): transaction to sign. - any other arguments are left to plugin authors to decide. - - Returns: - dict: transaction with any signatures applied. - """ - - @staticmethod - @abstractmethod - def validate_fulfillments(signed_transaction): - """Validate the fulfillments of a transaction. - - Args: - signed_transaction (dict): signed transaction to verify - - Returns: - bool: True if the transaction's required fulfillments are present - and correct, False otherwise. - """ - - @abstractmethod - def verify_vote_signature(block, signed_vote): - """Verify a cast vote. - - Args: - block (dict): block under election - signed_vote (dict): signed vote to verify - - Returns: - bool: True if the votes's required signature data is present - and correct, False otherwise. - """ - - -class BaseConsensusRules(AbstractConsensusRules): +class BaseConsensusRules(): """Base consensus rules for Bigchain. - - This class can be copied or overridden to write your own consensus rules! """ @staticmethod def validate_transaction(bigchain, transaction): - """Validate a transaction. + """See :meth:`bigchaindb.models.Transaction.validate` + for documentation. - Args: - bigchain (Bigchain): an instantiated bigchaindb.Bigchain object. - transaction (dict): transaction to validate. - - Returns: - The transaction if the transaction is valid else it raises an - exception describing the reason why the transaction is invalid. - - Raises: - OperationError: if the transaction operation is not supported - TransactionDoesNotExist: if the input of the transaction is not found - TransactionOwnerError: if the new transaction is using an input it doesn't own - DoubleSpend: if the transaction is a double spend - InvalidHash: if the hash of the transaction is wrong - InvalidSignature: if the signature of the transaction is wrong """ - - # If the operation is CREATE the transaction should have no inputs and - # should be signed by a federation node - if transaction['transaction']['operation'] in ('CREATE', 'GENESIS'): - # TODO: for now lets assume a CREATE transaction only has one fulfillment - if transaction['transaction']['fulfillments'][0]['input']: - raise ValueError('A CREATE operation has no inputs') - # TODO: for now lets assume a CREATE transaction only has one owner_before - if transaction['transaction']['fulfillments'][0]['owners_before'][0] not in ( - bigchain.nodes_except_me + [bigchain.me]): - raise exceptions.OperationError( - 'Only federation nodes can use the operation `CREATE`') - - else: - # check if the input exists, is owned by the owner_before - if not transaction['transaction']['fulfillments']: - raise ValueError('Transaction contains no fulfillments') - - # check inputs - for fulfillment in transaction['transaction']['fulfillments']: - if not fulfillment['input']: - raise ValueError('Only `CREATE` transactions can have null inputs') - tx_input = bigchain.get_transaction(fulfillment['input']['txid']) - - if not tx_input: - raise exceptions.TransactionDoesNotExist( - 'input `{}` does not exist in the bigchain'.format( - fulfillment['input']['txid'])) - # TODO: check if current owners own tx_input (maybe checked by InvalidSignature) - # check if the input was already spent by a transaction other than - # this one. - spent = bigchain.get_spent(fulfillment['input']) - if spent and spent['id'] != transaction['id']: - raise exceptions.DoubleSpend( - 'input `{}` was already spent'.format(fulfillment['input'])) - - # Check hash of the transaction - calculated_hash = util.get_hash_data(transaction) - if calculated_hash != transaction['id']: - raise exceptions.InvalidHash() - - # Check fulfillments - if not util.validate_fulfillments(transaction): - raise exceptions.InvalidSignature() - - return transaction + return transaction.validate(bigchain) @staticmethod def validate_block(bigchain, block): - """Validate a block. - - Args: - bigchain (Bigchain): an instantiated bigchaindb.Bigchain object. - block (dict): block to validate. - - Returns: - The block if the block is valid else it raises an exception - describing the reason why the block is invalid. - - Raises: - InvalidHash: if the hash of the block is wrong. - """ - - # Check if current hash is correct - calculated_hash = crypto.hash_data(util.serialize(block['block'])) - if calculated_hash != block['id']: - raise exceptions.InvalidHash() - - # Check if the block was created by a federation node - if block['block']['node_pubkey'] not in (bigchain.nodes_except_me + [bigchain.me]): - raise exceptions.OperationError('Only federation nodes can create blocks') - - # Check if block signature is valid - verifying_key = crypto.VerifyingKey(block['block']['node_pubkey']) - if not verifying_key.verify(util.serialize(block['block']), block['signature']): - raise exceptions.InvalidSignature('Invalid block signature') - - return block + """See :meth:`bigchaindb.models.Block.validate` for documentation.""" + return block.validate(bigchain) @staticmethod - def create_transaction(owner_before, owner_after, tx_input, operation, - payload=None): - """Create a new transaction - - Refer to the documentation of ``bigchaindb.util.create_tx`` - """ - - return util.create_tx(owner_before, owner_after, tx_input, operation, - payload) - - @staticmethod - def sign_transaction(transaction, private_key, bigchain=None): - """Sign a transaction - - Refer to the documentation of ``bigchaindb.util.sign_tx`` - """ - - return util.sign_tx(transaction, private_key, bigchain=bigchain) - - @staticmethod - def validate_fulfillments(signed_transaction): - """Validate the fulfillments of a transaction. - - Refer to the documentation of ``bigchaindb.util.validate_fulfillments`` - """ - - return util.validate_fulfillments(signed_transaction) - - @staticmethod - def verify_vote_signature(block, signed_vote): + def verify_vote_signature(voters, signed_vote): """Verify the signature of a vote. - Refer to the documentation of ``bigchaindb.util.verify_signature`` + Refer to the documentation of + :func:`bigchaindb.util.verify_signature`. """ - - return util.verify_vote_signature(block, signed_vote) + return verify_vote_signature(voters, signed_vote) diff --git a/bigchaindb/core.py b/bigchaindb/core.py index 4b5c17bc..e8e673ce 100644 --- a/bigchaindb/core.py +++ b/bigchaindb/core.py @@ -1,16 +1,20 @@ import random import math import collections -from copy import deepcopy 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 + import rethinkdb as r -import rapidjson import bigchaindb from bigchaindb.db.utils import Connection -from bigchaindb import config_utils, crypto, exceptions, util +from bigchaindb import config_utils, util +from bigchaindb.consensus import BaseConsensusRules +from bigchaindb.models import Block, Transaction class Bigchain(object): @@ -30,7 +34,7 @@ class Bigchain(object): def __init__(self, host=None, port=None, dbname=None, public_key=None, private_key=None, keyring=[], - consensus_plugin=None, backlog_reassign_delay=None): + backlog_reassign_delay=None): """Initialize the Bigchain instance A Bigchain instance has several configuration parameters (e.g. host). @@ -59,7 +63,7 @@ class Bigchain(object): self.me_private = private_key or bigchaindb.config['keypair']['private'] self.nodes_except_me = keyring or bigchaindb.config['keyring'] self.backlog_reassign_delay = backlog_reassign_delay or bigchaindb.config['backlog_reassign_delay'] - self.consensus = config_utils.load_consensus_plugin(consensus_plugin) + self.consensus = BaseConsensusRules # change RethinkDB read mode to majority. This ensures consistency in query results self.read_mode = 'majority' @@ -78,41 +82,6 @@ class Bigchain(object): def reconnect(self): return r.connect(host=self.host, port=self.port, db=self.dbname) - def create_transaction(self, *args, **kwargs): - """Create a new transaction - - Refer to the documentation of your consensus plugin. - - Returns: - dict: newly constructed transaction. - """ - - return self.consensus.create_transaction(*args, **kwargs) - - def sign_transaction(self, transaction, *args, **kwargs): - """Sign a transaction - - Refer to the documentation of your consensus plugin. - - Returns: - dict: transaction with any signatures applied. - """ - - return self.consensus.sign_transaction(transaction, *args, bigchain=self, **kwargs) - - def validate_fulfillments(self, signed_transaction, *args, **kwargs): - """Validate the fulfillment(s) of a transaction. - - Refer to the documentation of your consensus plugin. - - Returns: - bool: True if the transaction's required fulfillments are present - and correct, False otherwise. - """ - - return self.consensus.validate_fulfillments( - signed_transaction, *args, **kwargs) - def write_transaction(self, signed_transaction, durability='soft'): """Write the transaction to bigchain. @@ -120,25 +89,21 @@ class Bigchain(object): it has been validated by the nodes of the federation. Args: - signed_transaction (dict): transaction with the `signature` included. + signed_transaction (Transaction): transaction with the `signature` included. Returns: dict: database response """ + signed_transaction = signed_transaction.to_dict() # we will assign this transaction to `one` node. This way we make sure that there are no duplicate # transactions on the bigchain - if self.nodes_except_me: assignee = random.choice(self.nodes_except_me) else: # I am the only node assignee = self.me - # We copy the transaction here to not add `assignee` to the transaction - # dictionary passed to this method (as it would update by reference). - signed_transaction = deepcopy(signed_transaction) - # update the transaction signed_transaction.update({'assignee': assignee}) signed_transaction.update({'assignment_timestamp': time()}) @@ -190,6 +155,42 @@ class Bigchain(object): .filter(lambda tx: time() - tx['assignment_timestamp'] > self.backlog_reassign_delay).run(self.conn) + def validate_transaction(self, transaction): + """Validate a transaction. + + Args: + transaction (Transaction): transaction to validate. + + Returns: + The transaction if the transaction is valid else it raises an + exception describing the reason why the transaction is invalid. + """ + + return self.consensus.validate_transaction(self, transaction) + + def is_valid_transaction(self, transaction): + """Check whether a transaction is valid or invalid. + + Similar to :meth:`~bigchaindb.Bigchain.validate_transaction` + but never raises an exception. It returns :obj:`False` if + the transaction is invalid. + + Args: + transaction (:Class:`~bigchaindb.models.Transaction`): transaction + to check. + + Returns: + The :class:`~bigchaindb.models.Transaction` instance if valid, + otherwise :obj:`False`. + """ + + try: + return self.validate_transaction(transaction) + except (ValueError, exceptions.OperationError, exceptions.TransactionDoesNotExist, + exceptions.TransactionOwnerError, exceptions.DoubleSpend, + exceptions.InvalidHash, exceptions.InvalidSignature): + return False + def get_transaction(self, txid, include_status=False): """Retrieve a transaction with `txid` from bigchain. @@ -244,6 +245,9 @@ class Bigchain(object): if response: tx_status = self.TX_IN_BACKLOG + if response: + response = Transaction.from_dict(response) + if include_status: return response, tx_status else: @@ -271,7 +275,7 @@ class Bigchain(object): index (str): name of a secondary index, e.g. 'transaction_id' Returns: - A list of blocks with with only election information + :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( @@ -296,17 +300,25 @@ class Bigchain(object): # First, get information on all blocks which contain this transaction blocks = self.search_block_election_on_index(txid, 'transaction_id') - if blocks: # Determine the election status of each block - validity = {block['id']: self.block_election_status(block) for block in blocks} + validity = { + block['id']: self.block_election_status( + block['id'], + block['block']['voters'] + ) for block in blocks + } - # If there are multiple valid blocks with this transaction, something has gone wrong + # NOTE: If there are multiple valid blocks with this transaction, + # 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]) - raise Exception('Transaction {tx} is present in multiple valid blocks: {block_ids}' - .format(tx=txid, block_ids=block_ids)) + if validity[block] == Bigchain.BLOCK_VALID]) + raise exceptions.DoubleSpend('Transaction {tx} is present in ' + 'multiple valid blocks: ' + '{block_ids}' + .format(tx=txid, + block_ids=block_ids)) return validity @@ -319,7 +331,7 @@ class Bigchain(object): When creating a transaction one of the optional arguments is the `payload`. The payload is a generic dict that contains information about the digital asset. - To make it easy to query the bigchain for that digital asset we create a UUID for the payload and + To make it easy to query BigchainDB for that digital asset we create a UUID for the payload and store it with the transaction. This makes it easy for developers to keep track of their digital assets in bigchain. @@ -337,19 +349,21 @@ class Bigchain(object): .filter(lambda transaction: transaction['transaction']['data']['uuid'] == payload_uuid)) transactions = list(cursor) - return transactions + return [Transaction.from_dict(tx) for tx in transactions] - def get_spent(self, tx_input): + def get_spent(self, txid, cid): """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: - tx_input (dict): Input of a transaction in the form `{'txid': 'transaction id', 'cid': 'condition id'}` + txid (str): The id of the transaction + cid (num): the index of the condition in the respective transaction Returns: - The transaction that used the `txid` as an input if it exists else it returns `None` + The transaction (Transaction) that used the `txid` as an input else + `None` """ # checks if an input was already spent # checks if the bigchain has any transaction with input {'txid': ..., 'cid': ...} @@ -357,7 +371,7 @@ class Bigchain(object): 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'] == tx_input))) + .contains(lambda fulfillment: fulfillment['input'] == {'txid': txid, 'cid': cid}))) transactions = list(response) @@ -367,14 +381,15 @@ class Bigchain(object): num_valid_transactions = 0 for transaction in transactions: # ignore invalid blocks + # FIXME: Isn't there a faster solution than doing I/O again? 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( - tx_input['txid'])) + txid)) if num_valid_transactions: - return transactions[0] + return Transaction.from_dict(transactions[0]) else: # all queried transactions were invalid return None @@ -388,7 +403,8 @@ class Bigchain(object): owner (str): base58 encoded public key. Returns: - list: list of `txids` currently owned by `owner` + list (TransactionLink): list of `txid`s and `cid`s pointing to + another transaction's condition """ # get all transactions in which owner is in the `owners_after` list @@ -407,95 +423,49 @@ class Bigchain(object): if Bigchain.BLOCK_UNDECIDED not in validity.values(): continue + # NOTE: It's OK to not serialize the transaction here, as we do not + # 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 condition in tx['transaction']['conditions']: + for index, cond in enumerate(tx['transaction']['conditions']): # for simple signature conditions there are no subfulfillments # check if the owner is in the condition `owners_after` - if len(condition['owners_after']) == 1: - if condition['condition']['details']['public_key'] == owner: - tx_input = {'txid': tx['id'], 'cid': condition['cid']} + if len(cond['owners_after']) == 1: + if cond['condition']['details']['public_key'] == owner: + tx_link = TransactionLink(tx['id'], index) else: # for transactions with multiple `owners_after` there will be several subfulfillments nested # in the condition. We need to iterate the subfulfillments to make sure there is a # subfulfillment for `owner` - if util.condition_details_has_owner(condition['condition']['details'], owner): - tx_input = {'txid': tx['id'], 'cid': condition['cid']} + if util.condition_details_has_owner(cond['condition']['details'], owner): + tx_link = TransactionLink(tx['id'], index) # check if input was already spent - if not self.get_spent(tx_input): - owned.append(tx_input) + if not self.get_spent(tx_link.txid, tx_link.cid): + owned.append(tx_link) return owned - def validate_transaction(self, transaction): - """Validate a transaction. - - Args: - transaction (dict): transaction to validate. - - Returns: - The transaction if the transaction is valid else it raises an - exception describing the reason why the transaction is invalid. - """ - - return self.consensus.validate_transaction(self, transaction) - - def is_valid_transaction(self, transaction): - """Check whether a transacion is valid or invalid. - - Similar to `validate_transaction` but never raises an exception. - It returns `False` if the transaction is invalid. - - Args: - transaction (dict): transaction to check. - - Returns: - `transaction` if the transaction is valid, `False` otherwise - """ - - try: - self.validate_transaction(transaction) - return transaction - except (ValueError, exceptions.OperationError, exceptions.TransactionDoesNotExist, - exceptions.TransactionOwnerError, exceptions.DoubleSpend, - exceptions.InvalidHash, exceptions.InvalidSignature): - return False - def create_block(self, validated_transactions): """Creates a block given a list of `validated_transactions`. - Note that this method does not validate the transactions. Transactions should be validated before - calling create_block. + Note that this method does not validate the transactions. Transactions + should be validated before calling create_block. Args: - validated_transactions (list): list of validated transactions. + validated_transactions (list(Transaction)): list of validated + transactions. Returns: - dict: created block. + Block: created block. """ - # Prevent the creation of empty blocks if len(validated_transactions) == 0: - raise exceptions.OperationError('Empty block creation is not allowed') + raise exceptions.OperationError('Empty block creation is not ' + 'allowed') - # Create the new block - block = { - 'timestamp': util.timestamp(), - 'transactions': validated_transactions, - 'node_pubkey': self.me, - 'voters': self.nodes_except_me + [self.me] - } - - # Calculate the hash of the new block - block_data = util.serialize(block) - block_hash = crypto.hash_data(block_data) - block_signature = crypto.SigningKey(self.me_private).sign(block_data) - - block = { - 'id': block_hash, - 'block': block, - 'signature': block_signature, - } + voters = self.nodes_except_me + [self.me] + block = Block(validated_transactions, self.me, gen_timestamp(), voters) + block = block.sign(self.me_private) return block @@ -504,33 +474,20 @@ class Bigchain(object): """Validate a block. Args: - block (dict): block to validate. + block (Block): block to validate. Returns: The block if the block is valid else it raises and exception describing the reason why the block is invalid. """ - # First, make sure this node hasn't already voted on this block - if self.has_previous_vote(block): - return block + return self.consensus.validate_block(self, block) - # Run the plugin block validation logic - self.consensus.validate_block(self, block) - - # Finally: Tentative assumption that every blockchain will want to - # validate all transactions in each block - for transaction in block['block']['transactions']: - if not self.is_valid_transaction(transaction): - # this will raise the exception - self.validate_transaction(transaction) - - return block - - def has_previous_vote(self, block): + def has_previous_vote(self, block_id, voters): """Check for previous votes from this node Args: - block (dict): block to check. + block_id (str): the id of the block to check + voters (list(str)): the voters of the block to check Returns: bool: :const:`True` if this block already has a @@ -543,50 +500,31 @@ class Bigchain(object): """ votes = list(self.connection.run( r.table('votes', read_mode=self.read_mode) - .get_all([block['id'], self.me], index='block_and_voter'))) + .get_all([block_id, self.me], index='block_and_voter'))) if len(votes) > 1: raise exceptions.MultipleVotesError('Block {block_id} has {n_votes} votes from public key {me}' - .format(block_id=block['id'], n_votes=str(len(votes)), me=self.me)) + .format(block_id=block_id, n_votes=str(len(votes)), me=self.me)) has_previous_vote = False if votes: - if util.verify_vote_signature(block, votes[0]): + if util.verify_vote_signature(voters, votes[0]): has_previous_vote = True else: raise exceptions.ImproperVoteError('Block {block_id} already has an incorrectly signed vote ' - 'from public key {me}'.format(block_id=block['id'], me=self.me)) + 'from public key {me}'.format(block_id=block_id, me=self.me)) return has_previous_vote - def is_valid_block(self, block): - """Check whether a block is valid or invalid. - - Similar to `validate_block` but does not raise an exception if the block is invalid. - - Args: - block (dict): block to check. - - Returns: - bool: `True` if the block is valid, `False` otherwise. - """ - - try: - self.validate_block(block) - return True - except Exception: - return False - def write_block(self, block, durability='soft'): """Write a block to bigchain. Args: - block (dict): block to write to bigchain. + block (Block): block to write to bigchain. """ - block_serialized = rapidjson.dumps(block) self.connection.run( r.table('bigchain') - .insert(r.json(block_serialized), durability=durability)) + .insert(r.json(block.to_str()), durability=durability)) def transaction_exists(self, transaction_id): response = self.connection.run( @@ -598,14 +536,16 @@ class Bigchain(object): """Prepare a genesis block.""" payload = {'message': 'Hello World from the BigchainDB'} - transaction = self.create_transaction([self.me], [self.me], None, 'GENESIS', payload=payload) - transaction_signed = self.sign_transaction(transaction, self.me_private) + transaction = Transaction.create([self.me], [self.me], payload=payload) + + # NOTE: The transaction model doesn't expose an API to generate a + # GENESIS transaction, as this is literally the only usage. + transaction.operation = 'GENESIS' + transaction = transaction.sign([self.me_private]) # create the block - return self.create_block([transaction_signed]) + return self.create_block([transaction]) - # TODO: Unless we prescribe the signature of create_transaction, this will - # also need to be moved into the plugin API. def create_genesis_block(self): """Create the genesis block @@ -649,10 +589,10 @@ class Bigchain(object): 'previous_block': previous_block_id, 'is_block_valid': decision, 'invalid_reason': invalid_reason, - 'timestamp': util.timestamp() + 'timestamp': gen_timestamp() } - vote_data = util.serialize(vote) + vote_data = serialize(vote) signature = crypto.SigningKey(self.me_private).sign(vote_data) vote_signed = { @@ -687,9 +627,11 @@ class Bigchain(object): except r.ReqlNonExistenceError: # return last vote if last vote exists else return Genesis block - return list(self.connection.run( + res = self.connection.run( r.table('bigchain', read_mode=self.read_mode) - .filter(util.is_genesis_block)))[0] + .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 @@ -725,10 +667,14 @@ class Bigchain(object): r.table('bigchain', read_mode=self.read_mode) .get(last_block_id)) - return res + return Block.from_dict(res) def get_unvoted_blocks(self): - """Return all the blocks that has not been voted by this node.""" + """Return all the blocks that have not been voted on by this node. + + Returns: + :obj:`list` of :obj:`dict`: a list of unvoted blocks + """ unvoted = self.connection.run( r.table('bigchain', read_mode=self.read_mode) @@ -739,29 +685,28 @@ class Bigchain(object): # 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 = filter(lambda block: not util.is_genesis_block(block), unvoted) + unvoted_blocks = filter(lambda block: not util.is_genesis_block(block), unvoted) + return unvoted_blocks - return list(unvoted) - - def block_election_status(self, block): + 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')) + .between([block_id, r.minval], [block_id, r.maxval], index='block_and_voter')) votes = list(votes) - n_voters = len(block['block']['voters']) + 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)) + .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] @@ -770,7 +715,7 @@ class Bigchain(object): prev_block = [vote['vote']['previous_block'] for vote in votes] # vote_validity checks whether a vote is valid # or invalid, e.g. [False, True, True] - vote_validity = [self.consensus.verify_vote_signature(block, vote) for vote in votes] + vote_validity = [self.consensus.verify_vote_signature(voters, vote) for vote in votes] # element-wise product of stated vote and validity of vote # vote_cast = [True, True, False] and diff --git a/bigchaindb/crypto.py b/bigchaindb/crypto.py deleted file mode 100644 index 96be68e2..00000000 --- a/bigchaindb/crypto.py +++ /dev/null @@ -1,17 +0,0 @@ -# Separate all crypto code so that we can easily test several implementations - -import sha3 -from cryptoconditions import crypto - - -def hash_data(data): - """Hash the provided data using SHA3-256""" - return sha3.sha3_256(data.encode()).hexdigest() - - -def generate_key_pair(): - sk, pk = crypto.ed25519_generate_key_pair() - return sk.decode(), pk.decode() - -SigningKey = crypto.Ed25519SigningKey -VerifyingKey = crypto.Ed25519VerifyingKey diff --git a/bigchaindb/db/utils.py b/bigchaindb/db/utils.py index 84aabdd4..0ed74471 100644 --- a/bigchaindb/db/utils.py +++ b/bigchaindb/db/utils.py @@ -3,10 +3,10 @@ import time import logging +from bigchaindb_common import exceptions import rethinkdb as r import bigchaindb -from bigchaindb import exceptions logger = logging.getLogger(__name__) diff --git a/bigchaindb/exceptions.py b/bigchaindb/exceptions.py deleted file mode 100644 index 7b652fa3..00000000 --- a/bigchaindb/exceptions.py +++ /dev/null @@ -1,65 +0,0 @@ -"""Custom exceptions used in the `bigchaindb` package. -""" - -class ConfigurationError(Exception): - """Raised when there is a problem with server configuration""" - - -class OperationError(Exception): - """Raised when an operation cannot go through""" - - -class TransactionDoesNotExist(Exception): - """Raised if the transaction is not in the database""" - - -class TransactionOwnerError(Exception): - """Raised if a user tries to transfer a transaction they don't own""" - - -class DoubleSpend(Exception): - """Raised if a double spend is found""" - - -class InvalidHash(Exception): - """Raised if there was an error checking the hash for a particular operation""" - - -class InvalidSignature(Exception): - """Raised if there was an error checking the signature for a particular operation""" - - -class DatabaseAlreadyExists(Exception): - """Raised when trying to create the database but the db is already there""" - - -class DatabaseDoesNotExist(Exception): - """Raised when trying to delete the database but the db is not there""" - - -class KeypairNotFoundException(Exception): - """Raised if operation cannot proceed because the keypair was not given""" - - -class KeypairMismatchException(Exception): - """Raised if the private key(s) provided for signing don't match any of the curret owner(s)""" - - -class StartupError(Exception): - """Raised when there is an error starting up the system""" - - -class ImproperVoteError(Exception): - """Raised if a vote is not constructed correctly, or signed incorrectly""" - - -class MultipleVotesError(Exception): - """Raised if a voter has voted more than once""" - - -class GenesisBlockAlreadyExistsError(Exception): - """Raised when trying to create the already existing genesis block""" - - -class CyclicBlockchainError(Exception): - """Raised when there is a cycle in the blockchain""" diff --git a/bigchaindb/models.py b/bigchaindb/models.py new file mode 100644 index 00000000..6855787e --- /dev/null +++ b/bigchaindb/models.py @@ -0,0 +1,208 @@ +from bigchaindb_common.crypto import hash_data, VerifyingKey, SigningKey +from bigchaindb_common.exceptions import (InvalidHash, InvalidSignature, + OperationError, DoubleSpend, + TransactionDoesNotExist) +from bigchaindb_common.transaction import Transaction +from bigchaindb_common.util import gen_timestamp, serialize + + +class Transaction(Transaction): + def validate(self, bigchain): + """Validate a transaction. + + Args: + bigchain (Bigchain): an instantiated bigchaindb.Bigchain object. + + Returns: + The transaction (Transaction) if the transaction is valid else it + raises an exception describing the reason why the transaction is + invalid. + + Raises: + OperationError: if the transaction operation is not supported + TransactionDoesNotExist: if the input of the transaction is not + found + TransactionOwnerError: if the new transaction is using an input it + doesn't own + DoubleSpend: if the transaction is a double spend + InvalidHash: if the hash of the transaction is wrong + InvalidSignature: if the signature of the transaction is wrong + """ + if len(self.fulfillments) == 0: + raise ValueError('Transaction contains no fulfillments') + + input_conditions = [] + inputs_defined = all([ffill.tx_input for ffill in self.fulfillments]) + + if self.operation in (Transaction.CREATE, Transaction.GENESIS): + if inputs_defined: + raise ValueError('A CREATE operation has no inputs') + elif self.operation == Transaction.TRANSFER: + if not inputs_defined: + raise ValueError('Only `CREATE` transactions can have null ' + 'inputs') + + for ffill in self.fulfillments: + input_txid = ffill.tx_input.txid + input_cid = ffill.tx_input.cid + input_tx = bigchain.get_transaction(input_txid) + if input_tx is None: + raise TransactionDoesNotExist("input `{}` doesn't exist" + .format(input_txid)) + + spent = bigchain.get_spent(input_txid, ffill.tx_input.cid) + if spent and spent.id != self.id: + raise DoubleSpend('input `{}` was already spent' + .format(input_txid)) + + input_conditions.append(input_tx.conditions[input_cid]) + else: + allowed_operations = ', '.join(Transaction.ALLOWED_OPERATIONS) + raise TypeError('`operation`: `{}` must be either {}.' + .format(self.operation, allowed_operations)) + + if not self.fulfillments_valid(input_conditions): + raise InvalidSignature() + else: + return self + + +class Block(object): + def __init__(self, transactions=None, node_pubkey=None, timestamp=None, + voters=None, signature=None): + if transactions is not None and not isinstance(transactions, list): + raise TypeError('`transactions` must be a list instance or None') + else: + self.transactions = transactions or [] + + if voters is not None and not isinstance(voters, list): + raise TypeError('`voters` must be a list instance or None') + else: + self.voters = voters or [] + + if timestamp is not None: + self.timestamp = timestamp + else: + self.timestamp = gen_timestamp() + + self.node_pubkey = node_pubkey + self.signature = signature + + def __eq__(self, other): + try: + other = other.to_dict() + except AttributeError: + return False + return self.to_dict() == other + + def validate(self, bigchain): + """Validate a block. + + Args: + bigchain (Bigchain): an instantiated bigchaindb.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. + + Raises: + OperationError: if a non-federation node signed the block. + """ + + # First, make sure this node hasn't already voted on this block + if bigchain.has_previous_vote(self.id, self.voters): + return self + + # Check if the block was created by a federation node + possible_voters = (bigchain.nodes_except_me + [bigchain.me]) + if self.node_pubkey not in possible_voters: + raise OperationError('Only federation nodes can create blocks') + + if not self.is_signature_valid(): + raise InvalidSignature('Block signature invalid') + + # Finally: Tentative assumption that every blockchain will want to + # validate all transactions in each block + for tx in self.transactions: + # NOTE: If a transaction is not valid, `is_valid` will throw an + # an exception and block validation will be canceled. + bigchain.validate_transaction(tx) + + return self + + def sign(self, signing_key): + block_body = self.to_dict() + block_serialized = serialize(block_body['block']) + signing_key = SigningKey(signing_key) + self.signature = signing_key.sign(block_serialized) + return self + + def is_signature_valid(self): + block = self.to_dict()['block'] + block_serialized = serialize(block) + verifying_key = VerifyingKey(block['node_pubkey']) + try: + # NOTE: CC throws a `ValueError` on some wrong signatures + # https://github.com/bigchaindb/cryptoconditions/issues/27 + return verifying_key.verify(block_serialized, self.signature) + except (ValueError, AttributeError): + return False + + @classmethod + def from_dict(cls, block_body): + block = block_body['block'] + block_serialized = serialize(block) + block_id = hash_data(block_serialized) + verifying_key = VerifyingKey(block['node_pubkey']) + + try: + signature = block_body['signature'] + except KeyError: + signature = None + + if block_id != block_body['id']: + raise InvalidHash() + + if signature is not None: + # NOTE: CC throws a `ValueError` on some wrong signatures + # https://github.com/bigchaindb/cryptoconditions/issues/27 + try: + signature_valid = verifying_key.verify(block_serialized, + signature) + except ValueError: + signature_valid = False + if signature_valid is False: + raise InvalidSignature('Invalid block signature') + + transactions = [Transaction.from_dict(tx) for tx + in block['transactions']] + + return cls(transactions, block['node_pubkey'], + block['timestamp'], block['voters'], signature) + + @property + def id(self): + return self.to_dict()['id'] + + def to_dict(self): + if len(self.transactions) == 0: + raise OperationError('Empty block creation is not allowed') + + block = { + 'timestamp': self.timestamp, + 'transactions': [tx.to_dict() for tx in self.transactions], + 'node_pubkey': self.node_pubkey, + 'voters': self.voters, + } + block_serialized = serialize(block) + block_id = hash_data(block_serialized) + + return { + 'id': block_id, + 'block': block, + 'signature': self.signature, + } + + def to_str(self): + return serialize(self.to_dict()) diff --git a/bigchaindb/monitor.py b/bigchaindb/monitor.py index 3c14da1e..9d2bc527 100644 --- a/bigchaindb/monitor.py +++ b/bigchaindb/monitor.py @@ -30,4 +30,3 @@ class Monitor(statsd.StatsClient): if 'port' not in kwargs: kwargs['port'] = bigchaindb.config['statsd']['port'] super().__init__(*args, **kwargs) - diff --git a/bigchaindb/pipelines/block.py b/bigchaindb/pipelines/block.py index 69feb705..6876a7b7 100644 --- a/bigchaindb/pipelines/block.py +++ b/bigchaindb/pipelines/block.py @@ -1,6 +1,6 @@ """This module takes care of all the logic related to block creation. -The logic is encapsulated in the ``Block`` class, while the sequence +The logic is encapsulated in the ``BlockPipeline`` class, while the sequence of actions to do on transactions is specified in the ``create_pipeline`` function. """ @@ -10,6 +10,7 @@ import logging import rethinkdb as r from multipipes import Pipeline, Node +from bigchaindb.models import Transaction from bigchaindb.pipelines.utils import ChangeFeed from bigchaindb import Bigchain @@ -17,7 +18,7 @@ from bigchaindb import Bigchain logger = logging.getLogger(__name__) -class Block: +class BlockPipeline: """This class encapsulates the logic to create blocks. Note: @@ -25,7 +26,7 @@ class Block: """ def __init__(self): - """Initialize the Block creator""" + """Initialize the BlockPipeline creator""" self.bigchain = Bigchain() self.txs = [] @@ -36,10 +37,9 @@ class Block: tx (dict): the transaction to process. Returns: - The transaction if assigned to the current node, + dict: The transaction if assigned to the current node, ``None`` otherwise. """ - if tx['assignee'] == self.bigchain.me: tx.pop('assignee') tx.pop('assignment_timestamp') @@ -55,19 +55,21 @@ class Block: tx (dict): the transaction to validate. Returns: - The transaction if valid, ``None`` otherwise. + :class:`~bigchaindb.models.Transaction`: The transaction if valid, + ``None`` otherwise. """ - if self.bigchain.transaction_exists(tx['id']): + tx = Transaction.from_dict(tx) + if self.bigchain.transaction_exists(tx.id): # if the transaction already exists, we must check whether # it's in a valid or undecided block - tx, status = self.bigchain.get_transaction(tx['id'], + tx, status = self.bigchain.get_transaction(tx.id, include_status=True) if status == self.bigchain.TX_VALID \ or status == self.bigchain.TX_UNDECIDED: # 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. - r.table('backlog').get(tx['id']) \ + r.table('backlog').get(tx.id) \ .delete(durability='hard') \ .run(self.bigchain.conn) return None @@ -78,7 +80,7 @@ class Block: else: # if the transaction is not valid, remove it from the # backlog - r.table('backlog').get(tx['id']) \ + r.table('backlog').get(tx.id) \ .delete(durability='hard') \ .run(self.bigchain.conn) return None @@ -92,13 +94,14 @@ class Block: - a timeout happened. Args: - tx (dict): the transaction to validate, might be None if - a timeout happens. + tx (:class:`~bigchaindb.models.Transaction`): the transaction + to validate, might be None if a timeout happens. timeout (bool): ``True`` if a timeout happened (Default: ``False``). Returns: - The block, if a block is ready, or ``None``. + :class:`~bigchaindb.models.Block`: The block, + if a block is ready, or ``None``. """ if tx: self.txs.append(tx) @@ -111,14 +114,14 @@ class Block: """Write the block to the Database. Args: - block (dict): the block of transactions to write to the database. + block (:class:`~bigchaindb.models.Block`): the block of + transactions to write to the database. Returns: - The block. + :class:`~bigchaindb.models.Block`: The Block. """ - logger.info('Write new block %s with %s transactions', - block['id'], - len(block['block']['transactions'])) + logger.info('Write new block {} with {} transactions'.format(block.id, + len(block.transactions))) self.bigchain.write_block(block) return block @@ -126,13 +129,14 @@ class Block: """Delete transactions. Args: - block (dict): the block containg the transactions to delete. + block (:class:`~bigchaindb.models.Block`): the block + containg the transactions to delete. Returns: - The block. + :class:`~bigchaindb.models.Block`: The block. """ r.table('backlog')\ - .get_all(*[tx['id'] for tx in block['block']['transactions']])\ + .get_all(*[tx.id for tx in block.transactions])\ .delete(durability='hard')\ .run(self.bigchain.conn) @@ -166,24 +170,22 @@ def create_pipeline(): """Create and return the pipeline of operations to be distributed on different processes.""" - block = Block() + block_pipeline = BlockPipeline() - block_pipeline = Pipeline([ - Node(block.filter_tx), - Node(block.validate_tx, fraction_of_cores=1), - Node(block.create, timeout=1), - Node(block.write), - Node(block.delete_tx), + pipeline = Pipeline([ + Node(block_pipeline.filter_tx), + Node(block_pipeline.validate_tx, fraction_of_cores=1), + Node(block_pipeline.create, timeout=1), + Node(block_pipeline.write), + Node(block_pipeline.delete_tx), ]) - return block_pipeline + return pipeline def start(): """Create, start, and return the block pipeline.""" - pipeline = create_pipeline() pipeline.setup(indata=get_changefeed()) pipeline.start() return pipeline - diff --git a/bigchaindb/pipelines/election.py b/bigchaindb/pipelines/election.py index cf464e5c..49d44709 100644 --- a/bigchaindb/pipelines/election.py +++ b/bigchaindb/pipelines/election.py @@ -10,6 +10,7 @@ import rethinkdb as r from multipipes import Pipeline, Node from bigchaindb.pipelines.utils import ChangeFeed +from bigchaindb.models import Block from bigchaindb import Bigchain @@ -17,6 +18,7 @@ logger = logging.getLogger(__name__) class Election: + """Election class.""" def __init__(self): self.bigchain = Bigchain() @@ -24,22 +26,28 @@ class Election: def check_for_quorum(self, next_vote): """ Checks if block has enough invalid votes to make a decision + + Args: + next_vote: The next vote. + """ next_block = self.bigchain.connection.run( r.table('bigchain') .get(next_vote['vote']['voting_for_block'])) - if self.bigchain.block_election_status(next_block) == self.bigchain.BLOCK_INVALID: - return next_block + block_status = self.bigchain.block_election_status(next_block['id'], + next_block['block']['voters']) + if block_status == self.bigchain.BLOCK_INVALID: + return Block.from_dict(next_block) def requeue_transactions(self, invalid_block): """ Liquidates transactions from invalid blocks so they can be processed again """ logger.info('Rewriting %s transactions from invalid block %s', - len(invalid_block['block']['transactions']), - invalid_block['id']) - for tx in invalid_block['block']['transactions']: + len(invalid_block.transactions), + invalid_block.id) + for tx in invalid_block.transactions: self.bigchain.write_transaction(tx) return invalid_block diff --git a/bigchaindb/pipelines/vote.py b/bigchaindb/pipelines/vote.py index 6af8a291..3d79c265 100644 --- a/bigchaindb/pipelines/vote.py +++ b/bigchaindb/pipelines/vote.py @@ -8,22 +8,14 @@ function. from collections import Counter from multipipes import Pipeline, Node +from bigchaindb_common import exceptions -from bigchaindb import config_utils, exceptions +from bigchaindb.consensus import BaseConsensusRules +from bigchaindb.models import Transaction, Block from bigchaindb.pipelines.utils import ChangeFeed from bigchaindb import Bigchain -def create_invalid_tx(): - """Create and return an invalid transaction. - - The transaction is invalid because it's missing the signature.""" - - b = Bigchain() - tx = b.create_transaction(b.me, b.me, None, 'CREATE') - return tx - - class Vote: """This class encapsulates the logic to vote on blocks. @@ -37,35 +29,50 @@ class Vote: # Since cannot share a connection to RethinkDB using multiprocessing, # we need to create a temporary instance of BigchainDB that we use # only to query RethinkDB - last_voted = Bigchain().get_last_voted_block() - self.consensus = config_utils.load_consensus_plugin() + self.consensus = BaseConsensusRules # This is the Bigchain instance that will be "shared" (aka: copied) # by all the subprocesses self.bigchain = Bigchain() - self.last_voted_id = last_voted['id'] + self.last_voted_id = Bigchain().get_last_voted_block().id self.counters = Counter() self.validity = {} - self.invalid_dummy_tx = create_invalid_tx() + self.invalid_dummy_tx = Transaction.create([self.bigchain.me], + [self.bigchain.me]) def validate_block(self, block): - if not self.bigchain.has_previous_vote(block): + if not self.bigchain.has_previous_vote(block['id'], block['block']['voters']): + try: + block = Block.from_dict(block) + except (exceptions.InvalidHash, exceptions.InvalidSignature): + # XXX: if a block is invalid we should skip the `validate_tx` + # step, but since we are in a pipeline we cannot just jump to + # another function. Hackish solution: generate an invalid + # transaction and propagate it to the next steps of the + # pipeline. + return block['id'], [self.invalid_dummy_tx] try: self.consensus.validate_block(self.bigchain, block) - valid = True except (exceptions.InvalidHash, exceptions.OperationError, - exceptions.InvalidSignature) as e: - valid = False - return block, valid + exceptions.InvalidSignature): + # XXX: if a block is invalid we should skip the `validate_tx` + # step, but since we are in a pipeline we cannot just jump to + # another function. Hackish solution: generate an invalid + # transaction and propagate it to the next steps of the + # pipeline. + return block.id, [self.invalid_dummy_tx] + return block.id, block.transactions - def ungroup(self, block, valid): + def ungroup(self, block_id, transactions): """Given a block, ungroup the transactions in it. Args: - block (dict): the block to process + block_id (str): the id of the block in progress. + transactions (list(Transaction)): transactions of the block in + progress. Returns: ``None`` if the block has been already voted, an iterator that @@ -73,16 +80,9 @@ class Vote: transactions contained in the block otherwise. """ - # XXX: if a block is invalid we should skip the `validate_tx` step, - # but since we are in a pipeline we cannot just jump to another - # function. Hackish solution: generate an invalid transaction - # and propagate it to the next steps of the pipeline - if valid: - num_tx = len(block['block']['transactions']) - for tx in block['block']['transactions']: - yield tx, block['id'], num_tx - else: - yield self.invalid_dummy_tx, block['id'], 1 + num_tx = len(transactions) + for tx in transactions: + yield tx, block_id, num_tx def validate_tx(self, tx, block_id, num_tx): """Validate a transaction. diff --git a/bigchaindb/util.py b/bigchaindb/util.py index 8e7da785..3e84b4ba 100644 --- a/bigchaindb/util.py +++ b/bigchaindb/util.py @@ -1,6 +1,6 @@ -import copy import time import contextlib +from copy import deepcopy import threading import queue import multiprocessing as mp @@ -8,12 +8,13 @@ import uuid import rapidjson +from bigchaindb_common import crypto, exceptions +from bigchaindb_common.util import serialize import cryptoconditions as cc from cryptoconditions.exceptions import ParsingError import bigchaindb -from bigchaindb import exceptions -from bigchaindb import crypto +from bigchaindb.models import Transaction class ProcessGroup(object): @@ -93,440 +94,7 @@ def pool(builder, size, timeout=None): return pooled -def serialize(data): - """Serialize a dict into a JSON formatted string. - - This function enforces rules like the separator and order of keys. This ensures that all dicts - are serialized in the same way. - - This is specially important for hashing data. We need to make sure that everyone serializes their data - in the same way so that we do not have hash mismatches for the same structure due to serialization - differences. - - Args: - data (dict): dict to serialize - - Returns: - str: JSON formatted string - - """ - return rapidjson.dumps(data, skipkeys=False, ensure_ascii=False, sort_keys=True) - - -def deserialize(data): - """Deserialize a JSON formatted string into a dict. - - Args: - data (str): JSON formatted string. - - Returns: - dict: dict resulting from the serialization of a JSON formatted string. - """ - - return rapidjson.loads(data) - - -def timestamp(): - """The Unix time, rounded to the nearest second. - See https://en.wikipedia.org/wiki/Unix_time - - Returns: - str: the Unix time - """ - return str(round(time.time())) - - -# TODO: Consider remove the operation (if there are no inputs CREATE else TRANSFER) -def create_tx(owners_before, owners_after, inputs, operation, payload=None): - """Create a new transaction - - A transaction in the bigchain is a transfer of a digital asset between two entities represented - by public keys. - - Currently the bigchain supports two types of operations: - - `CREATE` - Only federation nodes are allowed to use this operation. In a create operation - a federation node creates a digital asset in the bigchain and assigns that asset to a public - key. The owner of the private key can then decided to transfer this digital asset by using the - `transaction id` of the transaction as an input in a `TRANSFER` transaction. - - `TRANSFER` - A transfer operation allows for a transfer of the digital assets between entities. - - Args: - owners_before (list): base58 encoded public key of the current owners of the asset. - owners_after (list): base58 encoded public key of the new owners of the digital asset. - inputs (list): id of the transaction to use as input. - operation (str): Either `CREATE` or `TRANSFER` operation. - payload (Optional[dict]): dictionary with information about asset. - - Returns: - dict: unsigned transaction. - - - Raises: - TypeError: if the optional ``payload`` argument is not a ``dict``. - - Reference: - { - "id": "", - "transaction": { - "version": "transaction version number", - "fulfillments": [ - { - "owners_before": ["list of "], - "input": { - "txid": "", - "cid": "condition index" - }, - "fulfillment": "fulfillement of condition cid", - "fid": "fulfillment index" - } - ], - "conditions": [ - { - "owners_after": ["list of "], - "condition": "condition to be met", - "cid": "condition index (1-to-1 mapping with fid)" - } - ], - "operation": "", - "timestamp": "", - "data": { - "hash": "", - "payload": { - "title": "The Winds of Plast", - "creator": "Johnathan Plunkett", - "IPFS_key": "QmfQ5QAjvg4GtA3wg3adpnDJug8ktA1BxurVqBD8rtgVjP" - } - } - }, - } - """ - # validate arguments (owners and inputs should be lists or None) - - # The None case appears on fulfilling a hashlock - if owners_before is None: - owners_before = [] - if not isinstance(owners_before, list): - owners_before = [owners_before] - - # The None case appears on assigning a hashlock - if owners_after is None: - owners_after = [] - if not isinstance(owners_after, list): - owners_after = [owners_after] - - if not isinstance(inputs, list): - inputs = [inputs] - - # handle payload - if payload is not None and not isinstance(payload, dict): - raise TypeError('`payload` must be an dict instance or None') - - data = { - 'uuid': str(uuid.uuid4()), - 'payload': payload - } - - # handle inputs - fulfillments = [] - - # transfer - if inputs: - for fid, tx_input in enumerate(inputs): - fulfillments.append({ - 'owners_before': owners_before, - 'input': tx_input, - 'fulfillment': None, - 'fid': fid - }) - # create - else: - fulfillments.append({ - 'owners_before': owners_before, - 'input': None, - 'fulfillment': None, - 'fid': 0 - }) - - # handle outputs - conditions = [] - for fulfillment in fulfillments: - - # threshold condition - if len(owners_after) > 1: - condition = cc.ThresholdSha256Fulfillment(threshold=len(owners_after)) - for owner_after in owners_after: - condition.add_subfulfillment(cc.Ed25519Fulfillment(public_key=owner_after)) - - # simple signature condition - elif len(owners_after) == 1: - condition = cc.Ed25519Fulfillment(public_key=owners_after[0]) - - # to be added later (hashlock conditions) - else: - condition = None - - if condition: - conditions.append({ - 'owners_after': owners_after, - 'condition': { - 'details': condition.to_dict(), - 'uri': condition.condition_uri - }, - 'cid': fulfillment['fid'] - }) - - tx = { - 'version': 1, - 'fulfillments': fulfillments, - 'conditions': conditions, - 'operation': operation, - 'timestamp': timestamp(), - 'data': data - } - - # serialize and convert to bytes - tx_hash = get_hash_data(tx) - - # create the transaction - transaction = { - 'id': tx_hash, - 'transaction': tx - } - - return transaction - - -def sign_tx(transaction, signing_keys, bigchain=None): - """Sign a transaction - - A transaction signed with the `owner_before` corresponding private key. - - Args: - transaction (dict): transaction to sign. - signing_keys (list): list of base58 encoded private keys to create the fulfillments of the transaction. - bigchain (obj): bigchain instance used to get the details of the previous transaction outputs. Useful - if the `Bigchain` instance was instantiated with parameters that override the config file. - - Returns: - dict: transaction with the `fulfillment` fields populated. - - """ - # validate sk - if not isinstance(signing_keys, list): - signing_keys = [signing_keys] - - # create a mapping between sk and vk so that we can match the private key to the owners_before - key_pairs = {} - for sk in signing_keys: - signing_key = crypto.SigningKey(sk) - vk = signing_key.get_verifying_key().to_ascii().decode() - key_pairs[vk] = signing_key - - tx = copy.deepcopy(transaction) - - bigchain = bigchain if bigchain is not None else bigchaindb.Bigchain() - - for fulfillment in tx['transaction']['fulfillments']: - fulfillment_message = get_fulfillment_message(transaction, fulfillment) - # TODO: avoid instantiation, pass as argument! - input_condition = get_input_condition(bigchain, fulfillment) - parsed_fulfillment = cc.Fulfillment.from_dict(input_condition['condition']['details']) - # for the case in which the type of fulfillment is not covered by this method - parsed_fulfillment_signed = parsed_fulfillment - - # single current owner - if isinstance(parsed_fulfillment, cc.Ed25519Fulfillment): - parsed_fulfillment_signed = fulfill_simple_signature_fulfillment(fulfillment, - parsed_fulfillment, - fulfillment_message, - key_pairs) - # multiple current owners - elif isinstance(parsed_fulfillment, cc.ThresholdSha256Fulfillment): - parsed_fulfillment_signed = fulfill_threshold_signature_fulfillment(fulfillment, - parsed_fulfillment, - fulfillment_message, - key_pairs) - - signed_fulfillment = parsed_fulfillment_signed.serialize_uri() - fulfillment.update({'fulfillment': signed_fulfillment}) - - return tx - - -def fulfill_simple_signature_fulfillment(fulfillment, parsed_fulfillment, fulfillment_message, key_pairs): - """Fulfill a cryptoconditions.Ed25519Fulfillment - - Args: - fulfillment (dict): BigchainDB fulfillment to fulfill. - parsed_fulfillment (cryptoconditions.Ed25519Fulfillment): cryptoconditions.Ed25519Fulfillment instance. - fulfillment_message (dict): message to sign. - key_pairs (dict): dictionary of (public_key, private_key) pairs. - - Returns: - object: fulfilled cryptoconditions.Ed25519Fulfillment - - """ - owner_before = fulfillment['owners_before'][0] - - try: - parsed_fulfillment.sign(serialize(fulfillment_message), key_pairs[owner_before]) - except KeyError: - raise exceptions.KeypairMismatchException('Public key {} is not a pair to any of the private keys' - .format(owner_before)) - - return parsed_fulfillment - - -def fulfill_threshold_signature_fulfillment(fulfillment, parsed_fulfillment, fulfillment_message, key_pairs): - """Fulfill a cryptoconditions.ThresholdSha256Fulfillment - - Args: - fulfillment (dict): BigchainDB fulfillment to fulfill. - parsed_fulfillment (cryptoconditions.ThresholdSha256Fulfillment): cryptoconditions.ThresholdSha256Fulfillment instance. - fulfillment_message (dict): message to sign. - key_pairs (dict): dictionary of (public_key, private_key) pairs. - - Returns: - object: fulfilled cryptoconditions.ThresholdSha256Fulfillment - - """ - parsed_fulfillment_copy = copy.deepcopy(parsed_fulfillment) - parsed_fulfillment.subconditions = [] - - for owner_before in fulfillment['owners_before']: - try: - subfulfillment = parsed_fulfillment_copy.get_subcondition_from_vk(owner_before)[0] - except IndexError: - raise exceptions.KeypairMismatchException( - 'Public key {} cannot be found in the fulfillment'.format(owner_before)) - try: - private_key = key_pairs[owner_before] - except KeyError: - raise exceptions.KeypairMismatchException( - 'Public key {} is not a pair to any of the private keys'.format(owner_before)) - - subfulfillment.sign(serialize(fulfillment_message), private_key) - parsed_fulfillment.add_subfulfillment(subfulfillment) - - return parsed_fulfillment - - -def create_and_sign_tx(private_key, owner_before, owner_after, tx_input, operation='TRANSFER', payload=None): - tx = create_tx(owner_before, owner_after, tx_input, operation, payload) - return sign_tx(tx, private_key) - - -def check_hash_and_signature(transaction): - # Check hash of the transaction - calculated_hash = get_hash_data(transaction) - if calculated_hash != transaction['id']: - raise exceptions.InvalidHash() - - # Check signature - if not validate_fulfillments(transaction): - raise exceptions.InvalidSignature() - - -def validate_fulfillments(signed_transaction): - """Verify the signature of a transaction - - A valid transaction should have been signed `owner_before` corresponding private key. - - Args: - signed_transaction (dict): a transaction with the `signature` included. - - Returns: - bool: True if the signature is correct, False otherwise. - """ - for fulfillment in signed_transaction['transaction']['fulfillments']: - fulfillment_message = get_fulfillment_message(signed_transaction, fulfillment) - try: - parsed_fulfillment = cc.Fulfillment.from_uri(fulfillment['fulfillment']) - except (TypeError, ValueError, ParsingError): - return False - - # TODO: might already break on a False here - is_valid = parsed_fulfillment.validate(message=serialize(fulfillment_message), - now=timestamp()) - - # if transaction has an input (i.e. not a `CREATE` transaction) - # TODO: avoid instantiation, pass as argument! - bigchain = bigchaindb.Bigchain() - input_condition = get_input_condition(bigchain, fulfillment) - is_valid = is_valid and parsed_fulfillment.condition_uri == input_condition['condition']['uri'] - - if not is_valid: - return False - - return True - - -def get_fulfillment_message(transaction, fulfillment, serialized=False): - """Get the fulfillment message for signing a specific fulfillment in a transaction - - Args: - transaction (dict): a transaction - fulfillment (dict): a specific fulfillment (for a condition index) within the transaction - serialized (Optional[bool]): False returns a dict, True returns a serialized string - - Returns: - str|dict: fulfillment message - """ - # data to sign contains common transaction data - fulfillment_message = { - 'operation': transaction['transaction']['operation'], - 'timestamp': transaction['transaction']['timestamp'], - 'data': transaction['transaction']['data'], - 'version': transaction['transaction']['version'], - 'id': transaction['id'] - } - # and the condition which needs to be retrieved from the output of a previous transaction - # or created on the fly it this is a `CREATE` transaction - fulfillment_message.update({ - 'fulfillment': copy.deepcopy(fulfillment), - 'condition': transaction['transaction']['conditions'][fulfillment['fid']] - }) - - # remove any fulfillment, as a fulfillment cannot sign itself - fulfillment_message['fulfillment']['fulfillment'] = None - - if serialized: - return serialize(fulfillment_message) - return fulfillment_message - - -def get_input_condition(bigchain, fulfillment): - """ - - Args: - bigchain: - fulfillment: - Returns: - """ - input_tx = fulfillment['input'] - # if `TRANSFER` transaction - if input_tx: - # get previous condition - previous_tx = bigchain.get_transaction(input_tx['txid']) - conditions = sorted(previous_tx['transaction']['conditions'], key=lambda d: d['cid']) - return conditions[input_tx['cid']] - - # if `CREATE` transaction - # there is no previous transaction so we need to create one on the fly - else: - owner_before = fulfillment['owners_before'][0] - condition = cc.Ed25519Fulfillment(public_key=owner_before) - - return { - 'condition': { - 'details': condition.to_dict(), - 'uri': condition.condition_uri - } - } - - +# TODO: Rename this function, it's handling fulfillments not conditions def condition_details_has_owner(condition_details, owner): """ @@ -558,33 +126,13 @@ def condition_details_has_owner(condition_details, owner): return False -def get_hash_data(transaction): - """ Get the hashed data that (should) correspond to the `transaction['id']` - - Args: - transaction (dict): the transaction to be hashed - - Returns: - str: the hash of the transaction - """ - tx = copy.deepcopy(transaction) - if 'transaction' in tx: - tx = tx['transaction'] - - # remove the fulfillment messages (signatures) - for fulfillment in tx['fulfillments']: - fulfillment['fulfillment'] = None - - return crypto.hash_data(serialize(tx)) - - -def verify_vote_signature(block, signed_vote): +def verify_vote_signature(voters, signed_vote): """Verify the signature of a vote A valid vote should have been signed `owner_before` corresponding private key. Args: - block (dict): block under election + voters (list): voters of the block that is under election signed_vote (dict): a vote with the `signature` included. Returns: @@ -595,32 +143,18 @@ def verify_vote_signature(block, signed_vote): vk_base58 = signed_vote['node_pubkey'] # immediately return False if the voter is not in the block voter list - if vk_base58 not in block['block']['voters']: + if vk_base58 not in voters: return False public_key = crypto.VerifyingKey(vk_base58) return public_key.verify(serialize(signed_vote['vote']), signature) -def transform_create(tx): - """Change the owner and signature for a ``CREATE`` transaction created by a node""" - - # XXX: the next instruction opens a new connection to the DB, consider using a singleton or a global - # if you need a Bigchain instance. - b = bigchaindb.Bigchain() - transaction = tx['transaction'] - payload = None - if transaction['data'] and 'payload' in transaction['data']: - payload = transaction['data']['payload'] - new_tx = create_tx(b.me, transaction['fulfillments'][0]['owners_before'], None, 'CREATE', payload=payload) - return new_tx - - def is_genesis_block(block): """Check if the block is the genesis block. Args: - block (dict): the block to check + block (dict | Block): the block to check Returns: bool: True if the block is the genesis block, False otherwise. @@ -628,5 +162,8 @@ def is_genesis_block(block): # we cannot have empty blocks, there will always be at least one # element in the list so we can safely refer to it - return block['block']['transactions'][0]['transaction']['operation'] == 'GENESIS' - + # TODO: Remove this try-except and only handle `Block` as input + try: + return block.transactions[0].operation == 'GENESIS' + except AttributeError: + return block['block']['transactions'][0]['transaction']['operation'] == 'GENESIS' diff --git a/bigchaindb/web/server.py b/bigchaindb/web/server.py index 43c4d6c1..50ef4cc3 100644 --- a/bigchaindb/web/server.py +++ b/bigchaindb/web/server.py @@ -91,4 +91,3 @@ def create_server(settings): app = create_app(settings) standalone = StandaloneApplication(app, settings) return standalone - diff --git a/bigchaindb/web/views/info.py b/bigchaindb/web/views/info.py index a6602657..fab7b7db 100644 --- a/bigchaindb/web/views/info.py +++ b/bigchaindb/web/views/info.py @@ -23,4 +23,3 @@ def home(): 'keyring': bigchaindb.config['keyring'], 'api_endpoint': bigchaindb.config['api_endpoint'] }) - diff --git a/bigchaindb/web/views/transactions.py b/bigchaindb/web/views/transactions.py index 647063b9..e1986d0a 100644 --- a/bigchaindb/web/views/transactions.py +++ b/bigchaindb/web/views/transactions.py @@ -6,8 +6,10 @@ For more information please refer to the documentation on ReadTheDocs: from flask import current_app, request, Blueprint from flask_restful import Resource, Api +from bigchaindb_common.exceptions import InvalidHash, InvalidSignature + import bigchaindb -from bigchaindb import util +from bigchaindb.models import Transaction from bigchaindb.web.views.base import make_error @@ -54,7 +56,7 @@ class TransactionApi(Resource): if not tx: return make_error(404) - return tx + return tx.to_dict() class TransactionStatusApi(Resource): @@ -94,17 +96,19 @@ class TransactionListApi(Resource): # set to `application/json` tx = request.get_json(force=True) + try: + tx_obj = Transaction.from_dict(tx) + except (InvalidHash, InvalidSignature): + return make_error(400, 'Invalid transaction') + with pool() as bigchain: - if tx['transaction']['operation'] == 'CREATE': - tx = util.transform_create(tx) - tx = bigchain.consensus.sign_transaction(tx, private_key=bigchain.me_private) - - if not bigchain.is_valid_transaction(tx): + if bigchain.is_valid_transaction(tx_obj): + rate = bigchaindb.config['statsd']['rate'] + with monitor.timer('write_transaction', rate=rate): + bigchain.write_transaction(tx_obj) + else: return make_error(400, 'Invalid transaction') - with monitor.timer('write_transaction', rate=bigchaindb.config['statsd']['rate']): - bigchain.write_transaction(tx) - return tx transaction_api.add_resource(TransactionApi, diff --git a/deploy-cluster-aws/write_keypairs_file.py b/deploy-cluster-aws/write_keypairs_file.py index 6af96e00..2037141c 100644 --- a/deploy-cluster-aws/write_keypairs_file.py +++ b/deploy-cluster-aws/write_keypairs_file.py @@ -1,5 +1,5 @@ """A Python 3 script to write a file with a specified number -of keypairs, using bigchaindb.crypto.generate_key_pair() +of keypairs, using bigchaindb_common.crypto.generate_key_pair() The written file is always named keypairs.py and it should be interpreted as a Python 2 script. @@ -16,7 +16,7 @@ Using the list in other Python scripts: import argparse -from bigchaindb import crypto +from bigchaindb_common import crypto # Parse the command-line arguments diff --git a/docs.yml b/docs.yml new file mode 100644 index 00000000..f3f5efcb --- /dev/null +++ b/docs.yml @@ -0,0 +1,16 @@ +version: '2' + +services: + bdocs: + build: + context: . + dockerfile: ./Dockerfile-dev + volumes: + - .:/usr/src/app/ + command: make -C docs html + vdocs: + image: nginx + ports: + - '33333:80' + volumes: + - ./docs/build/html:/usr/share/nginx/html diff --git a/docs/source/appendices/consensus.md b/docs/source/appendices/consensus.md deleted file mode 100644 index 72146859..00000000 --- a/docs/source/appendices/consensus.md +++ /dev/null @@ -1,82 +0,0 @@ -# BigchainDB Consensus Plugins - -BigchainDB has a pluggable block/transaction validation architecture. The default consensus rules can be extended or replaced entirely. - - -## Installing a plugin - -Plugins can be installed via pip! - -```bash -$ pip install bigchaindb-plugin-demo -``` - -Or using setuptools: - -```bash -$ cd bigchaindb-plugin-demo/ -$ python setup.py install # (or develop) -``` - -To activate your plugin, you can either set the `consensus_plugin` field in your config file (usually `~/.bigchaindb`) or by setting the `BIGCHAIN_CONSENSUS_PLUGIN` environement variable to the name of your plugin (see the section on [Packaging a plugin](#packaging-a-plugin) for more about plugin names). - - -## Plugin API - -BigchainDB's [current plugin API](https://github.com/bigchaindb/bigchaindb/blob/master/bigchaindb/consensus.py) exposes five functions in an `AbstractConsensusRules` class: - -```python -validate_transaction(bigchain, transaction) -validate_block(bigchain, block) -create_transaction(*args, **kwargs) -sign_transaction(transaction, *args, **kwargs) -validate_fulfillments(transaction) -``` - -Together, these functions are sufficient for most customizations. For example: -- Replace the crypto-system with one your hardware can accelerate -- Re-implement an existing protocol -- Delegate validation to another application -- etc... - - -## Extending BigchainDB behavior - -A default installation of BigchainDB will use the rules in the `BaseConsensusRules` class. If you only need to modify this behavior slightly, you can inherit from that class and call `super()` in any methods you change, so long as the return values remain the same. - -Here's a quick example of a plugin that adds nonsense rules: - -```python -from bigchaindb.consensus import BaseConsensusRules - -class SillyConsensusRules(BaseConsensusRules): - - @staticmethod - def validate_transaction(bigchain, transaction): - transaction = super().validate_transaction(bigchain, transaction) - # I only like transactions whose timestamps are even. - if transaction['transaction']['timestamp'] % 2 != 0: - raise StandardError("Odd... very odd indeed.") - return transaction - - @staticmethod - def validate_block(bigchain, block): - block = super().validate_block(bigchain, block) - # I don't trust Alice, I think she's shady. - if block['block']['node_pubkey'] == '': - raise StandardError("Alice is shady, everybody ignore her blocks!") - return block -``` - - -## Packaging a plugin - -BigchainDB uses [setuptools](https://setuptools.readthedocs.io/en/latest/)' entry_points to provide the plugin functionality. Any custom plugin needs to add this section to the `setup()` call in their `setup.py`: - -```python -entry_points={ - 'bigchaindb.consensus': [ - 'PLUGIN_NAME=package.module:ConsensusRulesClass' - ] -}, -``` diff --git a/docs/source/appendices/consensus.rst b/docs/source/appendices/consensus.rst new file mode 100644 index 00000000..837e81d5 --- /dev/null +++ b/docs/source/appendices/consensus.rst @@ -0,0 +1,6 @@ +######### +Consensus +######### + +.. automodule:: bigchaindb.consensus + :members: diff --git a/docs/source/appendices/index.rst b/docs/source/appendices/index.rst index 5e9133ae..c251db09 100755 --- a/docs/source/appendices/index.rst +++ b/docs/source/appendices/index.rst @@ -13,10 +13,11 @@ Appendices json-serialization cryptography the-Bigchain-class - aws-setup consensus + pipelines + aws-setup firewall-notes ntp-notes example-rethinkdb-storage-setups licenses - install-with-lxd \ No newline at end of file + install-with-lxd diff --git a/docs/source/appendices/pipelines.rst b/docs/source/appendices/pipelines.rst new file mode 100644 index 00000000..2685d2d3 --- /dev/null +++ b/docs/source/appendices/pipelines.rst @@ -0,0 +1,37 @@ +######### +Pipelines +######### + +Block Creation +============== + +.. automodule:: bigchaindb.pipelines.block + :members: + + +Block Voting +============ + +.. automodule:: bigchaindb.pipelines.vote + :members: + + +Block Status +============ + +.. automodule:: bigchaindb.pipelines.election + :members: + + +Stale Transaction Monitoring +============================ + +.. automodule:: bigchaindb.pipelines.stale + :members: + + +Utilities +========= + +.. automodule:: bigchaindb.pipelines.utils + :members: diff --git a/docs/source/server-reference/configuration.md b/docs/source/server-reference/configuration.md index b14a0054..cce5b6b8 100644 --- a/docs/source/server-reference/configuration.md +++ b/docs/source/server-reference/configuration.md @@ -20,7 +20,6 @@ For convenience, here's a list of all the relevant environment variables (docume `BIGCHAINDB_SERVER_WORKERS`
`BIGCHAINDB_SERVER_THREADS`
`BIGCHAINDB_API_ENDPOINT`
-`BIGCHAINDB_CONSENSUS_PLUGIN`
`BIGCHAINDB_STATSD_HOST`
`BIGCHAINDB_STATSD_PORT`
`BIGCHAINDB_STATSD_RATE`
@@ -163,21 +162,6 @@ export BIGCHAINDB_API_ENDPOINT="http://localhost:9984/api/v1" ``` -## consensus_plugin - -The [consensus plugin](../appendices/consensus.html) to use. - -**Example using an environment variable** -```text -export BIGCHAINDB_CONSENSUS_PLUGIN=default -``` - -**Example config file snippet: the default** -```js -"consensus_plugin": "default" -``` - - ## statsd.host, statsd.port & statsd.rate These settings are used to configure where, and how often, [StatsD](https://github.com/etsy/statsd) should send data for [cluster monitoring](../clusters-feds/monitoring.html) purposes. `statsd.host` is the hostname of the monitoring server, where StatsD should send its data. `stats.port` is the port. `statsd.rate` is the fraction of transaction operations that should be sampled. It's a float between 0.0 and 1.0. diff --git a/setup.py b/setup.py index 9aa9561b..b003abbe 100644 --- a/setup.py +++ b/setup.py @@ -88,9 +88,6 @@ setup( 'console_scripts': [ 'bigchaindb=bigchaindb.commands.bigchain:main' ], - 'bigchaindb.consensus': [ - 'default=bigchaindb.consensus:BaseConsensusRules' - ] }, install_requires=[ 'rethinkdb~=2.3', @@ -106,6 +103,7 @@ setup( 'requests~=2.9', 'gunicorn~=19.0', 'multipipes~=0.1.0', + 'bigchaindb-common>=0.0.2', ], setup_requires=['pytest-runner'], tests_require=tests_require, diff --git a/speed-tests/speed_tests.py b/speed-tests/speed_tests.py index 5af7fe5f..87a81b0f 100644 --- a/speed-tests/speed_tests.py +++ b/speed-tests/speed_tests.py @@ -6,6 +6,8 @@ from line_profiler import LineProfiler import bigchaindb +# BIG TODO: Adjust for new transaction model + def speedtest_validate_transaction(): # create a transaction diff --git a/tests/conftest.py b/tests/conftest.py index 76c5ed03..73bd025f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -68,3 +68,21 @@ def b(request, node_config): from bigchaindb import Bigchain return Bigchain() + +@pytest.fixture +def create_tx(b, user_vk): + from bigchaindb.models import Transaction + return Transaction.create([b.me], [user_vk]) + + +@pytest.fixture +def signed_create_tx(b, create_tx): + return create_tx.sign([b.me_private]) + + +@pytest.fixture +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]) + return tx.sign([user_sk]) diff --git a/tests/db/conftest.py b/tests/db/conftest.py index d89ca004..c6bcbe44 100644 --- a/tests/db/conftest.py +++ b/tests/db/conftest.py @@ -49,8 +49,8 @@ def setup_database(request, node_config): 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( - 'payload_uuid', - r.row['block']['transactions']['transaction']['data']['uuid'], + 'payload_uuid', + r.row['block']['transactions']['transaction']['data']['uuid'], multi=True, ).run() # compound index to read transactions from the backlog per assignee @@ -59,7 +59,7 @@ def setup_database(request, node_config): .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() + [r.row['vote']['voting_for_block'], r.row['node_pubkey']]).run() # order transactions by id r.db(db_name).table('bigchain').index_create('transaction_id', r.row['block']['transactions']['id'], multi=True).run() @@ -99,7 +99,8 @@ def cleanup_tables(request, node_config): @pytest.fixture def inputs(user_vk): - from bigchaindb.exceptions import GenesisBlockAlreadyExistsError + from bigchaindb.models import Transaction + from bigchaindb_common.exceptions import GenesisBlockAlreadyExistsError # 1. create the genesis block b = Bigchain() try: @@ -109,11 +110,10 @@ def inputs(user_vk): # 2. create block with transactions for `USER` to spend for block in range(4): - transactions = [] - for i in range(10): - tx = b.create_transaction(b.me, user_vk, None, 'CREATE') - tx_signed = b.sign_transaction(tx, b.me_private) - transactions.append(tx_signed) - + transactions = [ + Transaction.create( + [b.me], [user_vk], payload={'i': i}).sign([b.me_private]) + for i in range(10) + ] block = b.create_block(transactions) b.write_block(block, durability='hard') diff --git a/tests/db/test_bigchain_api.py b/tests/db/test_bigchain_api.py index fa01f6b2..01944b6e 100644 --- a/tests/db/test_bigchain_api.py +++ b/tests/db/test_bigchain_api.py @@ -1,12 +1,4 @@ -import copy -import time - import pytest -import rethinkdb as r -import cryptoconditions as cc - -import bigchaindb -from bigchaindb import crypto, exceptions, util @pytest.mark.skipif(reason='Some tests throw a ResourceWarning that might result in some weird ' @@ -16,90 +8,199 @@ def test_remove_unclosed_sockets(): pass -# Some util functions +# TODO: Get rid of this and move to conftest def dummy_tx(): + import bigchaindb + from bigchaindb.models import Transaction b = bigchaindb.Bigchain() - tx = b.create_transaction(b.me, b.me, None, 'CREATE') - tx_signed = b.sign_transaction(tx, b.me_private) - return tx_signed + tx = Transaction.create([b.me], [b.me]) + tx = tx.sign([b.me_private]) + return tx +# TODO: Get rid of this and move to conftest def dummy_block(): + import bigchaindb b = bigchaindb.Bigchain() block = b.create_block([dummy_tx()]) return block class TestBigchainApi(object): - def test_create_transaction_create(self, b, user_sk): - tx = b.create_transaction(b.me, user_sk, None, 'CREATE') + def test_get_last_voted_block_cyclic_blockchain(self, b, monkeypatch): + from bigchaindb_common.crypto import SigningKey + from bigchaindb_common.exceptions import CyclicBlockchainError + from bigchaindb_common.util import serialize + from bigchaindb.models import Transaction - assert sorted(tx) == ['id', 'transaction'] - assert sorted(tx['transaction']) == ['conditions', 'data', 'fulfillments', 'operation', 'timestamp', 'version'] + b.create_genesis_block() - def test_create_transaction_with_unsupported_payload_raises(self, b): - with pytest.raises(TypeError): - b.create_transaction('a', 'b', 'c', 'd', payload=[]) + tx = Transaction.create([b.me], [b.me]) + tx = tx.sign([b.me_private]) + monkeypatch.setattr('time.time', lambda: 1) + block1 = b.create_block([tx]) + b.write_block(block1, durability='hard') - def test_create_transaction_payload_none(self, b, user_vk): - tx = b.create_transaction(b.me, user_vk, None, 'CREATE') - assert len(tx['transaction']['data']['uuid']) == 36 - assert tx['transaction']['data']['payload'] is None + # Manipulate vote to create a cyclic Blockchain + vote = b.vote(block1.id, b.get_last_voted_block().id, True) + vote['vote']['previous_block'] = block1.id + vote_data = serialize(vote['vote']) + vote['signature'] = SigningKey(b.me_private).sign(vote_data) + b.write_vote(vote) - def test_create_transaction_payload(self, b, user_vk): - payload = {'msg': 'Hello BigchainDB!'} - tx = b.create_transaction(b.me, user_vk, None, 'CREATE', payload=payload) - assert len(tx['transaction']['data']['uuid']) == 36 - assert tx['transaction']['data']['payload'] == payload + with pytest.raises(CyclicBlockchainError): + b.get_last_voted_block() + + def test_try_voting_while_constructing_cyclic_blockchain(self, b, + monkeypatch): + from bigchaindb_common.exceptions import CyclicBlockchainError + from bigchaindb.models import Transaction + + b.create_genesis_block() + + tx = Transaction.create([b.me], [b.me]) + tx = tx.sign([b.me_private]) + block1 = b.create_block([tx]) + + # We can simply submit twice the same block id and check if `Bigchain` + # throws + with pytest.raises(CyclicBlockchainError): + b.vote(block1.id, block1.id, True) + + def test_has_previous_vote_when_already_voted(self, b, monkeypatch): + from bigchaindb.models import Transaction + + b.create_genesis_block() + + tx = Transaction.create([b.me], [b.me]) + tx = tx.sign([b.me_private]) + + monkeypatch.setattr('time.time', lambda: 1) + block = b.create_block([tx]) + b.write_block(block, durability='hard') + + assert b.has_previous_vote(block.id, block.voters) is False + + vote = b.vote(block.id, b.get_last_voted_block().id, True) + b.write_vote(vote) + + assert b.has_previous_vote(block.id, block.voters) is True + + def test_get_spent_with_double_spend(self, b, monkeypatch): + from bigchaindb_common.exceptions import DoubleSpend + from bigchaindb.models import Transaction + + b.create_genesis_block() + + tx = Transaction.create([b.me], [b.me]) + tx = tx.sign([b.me_private]) + + monkeypatch.setattr('time.time', lambda: 1) + block1 = b.create_block([tx]) + b.write_block(block1, durability='hard') + + monkeypatch.setattr('time.time', lambda: 2) + transfer_tx = Transaction.transfer(tx.to_inputs(), [b.me]) + 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]) + transfer_tx2 = transfer_tx2.sign([b.me_private]) + block3 = b.create_block([transfer_tx2]) + b.write_block(block3, durability='hard') + + # Vote both block2 and block3 valid to provoke a double spend + vote = b.vote(block2.id, b.get_last_voted_block().id, True) + b.write_vote(vote) + vote = b.vote(block3.id, b.get_last_voted_block().id, True) + b.write_vote(vote) + + with pytest.raises(DoubleSpend): + b.get_spent(tx.id, 0) + + def test_get_block_status_for_tx_with_double_spend(self, b, monkeypatch): + from bigchaindb_common.exceptions import DoubleSpend + from bigchaindb.models import Transaction + + b.create_genesis_block() + + tx = Transaction.create([b.me], [b.me]) + tx = tx.sign([b.me_private]) + + monkeypatch.setattr('time.time', lambda: 1) + block1 = b.create_block([tx]) + b.write_block(block1, durability='hard') + + monkeypatch.setattr('time.time', lambda: 2) + block2 = b.create_block([tx]) + b.write_block(block2, durability='hard') + + # Vote both blocks valid (creating a double spend) + vote = b.vote(block1.id, b.get_last_voted_block().id, True) + b.write_vote(vote) + vote = b.vote(block2.id, b.get_last_voted_block().id, True) + b.write_vote(vote) + + with pytest.raises(DoubleSpend): + b.get_blocks_status_containing_tx(tx.id) + + def test_get_transaction_in_invalid_and_valid_block(self, monkeypatch, b): + from bigchaindb.models import Transaction + + b.create_genesis_block() + + monkeypatch.setattr('time.time', lambda: 1) + tx1 = Transaction.create([b.me], [b.me]) + 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 = tx2.sign([b.me_private]) + block2 = b.create_block([tx2]) + b.write_block(block2, durability='hard') + + # vote the first block invalid + vote = b.vote(block1.id, b.get_last_voted_block().id, False) + b.write_vote(vote) + + # vote the second block valid + vote = b.vote(block2.id, b.get_last_voted_block().id, True) + b.write_vote(vote) + + assert b.get_transaction(tx1.id) == None + assert b.get_transaction(tx2.id) == tx2 def test_get_transactions_for_payload(self, b, user_vk): + from bigchaindb.models import Transaction + payload = {'msg': 'Hello BigchainDB!'} - tx = b.create_transaction(b.me, user_vk, None, 'CREATE', payload=payload) - payload_uuid = tx['transaction']['data']['uuid'] + tx = Transaction.create([b.me], [user_vk], payload=payload) block = b.create_block([tx]) b.write_block(block, durability='hard') - matches = b.get_tx_by_payload_uuid(payload_uuid) + matches = b.get_tx_by_payload_uuid(tx.data.payload_id) assert len(matches) == 1 - assert matches[0]['id'] == tx['id'] + assert matches[0].id == tx.id def test_get_transactions_for_payload_mismatch(self, b, user_vk): matches = b.get_tx_by_payload_uuid('missing') assert not matches - @pytest.mark.usefixtures('inputs') - def test_create_transaction_transfer(self, b, user_vk, user_sk): - input_tx = b.get_owned_ids(user_vk).pop() - assert b.validate_fulfillments(b.get_transaction(input_tx['txid'])) == True - - tx = b.create_transaction(user_vk, b.me, input_tx, 'TRANSFER') - - assert sorted(tx) == ['id', 'transaction'] - assert sorted(tx['transaction']) == ['conditions', 'data', 'fulfillments', 'operation', 'timestamp', 'version'] - - tx_signed = b.sign_transaction(tx, user_sk) - - assert b.validate_fulfillments(tx) == False - assert b.validate_fulfillments(tx_signed) == True - - def test_transaction_signature(self, b, user_sk, user_vk): - tx = b.create_transaction(user_vk, user_vk, None, 'CREATE') - tx_signed = b.sign_transaction(tx, user_sk) - - assert tx_signed['transaction']['fulfillments'][0]['fulfillment'] is not None - assert b.validate_fulfillments(tx_signed) - - def test_serializer(self, b, user_vk): - tx = b.create_transaction(user_vk, user_vk, None, 'CREATE') - assert util.deserialize(util.serialize(tx)) == tx - @pytest.mark.usefixtures('inputs') def test_write_transaction(self, b, user_vk, user_sk): + from bigchaindb.models import Transaction + input_tx = b.get_owned_ids(user_vk).pop() - tx = b.create_transaction(user_vk, user_vk, input_tx, 'TRANSFER') - tx_signed = b.sign_transaction(tx, user_sk) - response = b.write_transaction(tx_signed) + input_tx = b.get_transaction(input_tx.txid) + inputs = input_tx.to_inputs() + tx = Transaction.transfer(inputs, [user_vk]) + tx = tx.sign([user_sk]) + response = b.write_transaction(tx) assert response['skipped'] == 0 assert response['deleted'] == 0 @@ -109,107 +210,54 @@ class TestBigchainApi(object): assert response['inserted'] == 1 @pytest.mark.usefixtures('inputs') - def test_read_transaction_undecided_block(self, b, user_vk, user_sk): + def test_read_transaction(self, b, user_vk, user_sk): + from bigchaindb.models import Transaction + input_tx = b.get_owned_ids(user_vk).pop() - tx = b.create_transaction(user_vk, user_vk, input_tx, 'TRANSFER') - tx_signed = b.sign_transaction(tx, user_sk) + input_tx = b.get_transaction(input_tx.txid) + inputs = input_tx.to_inputs() + tx = Transaction.transfer(inputs, [user_vk]) + tx = tx.sign([user_sk]) + b.write_transaction(tx) # create block and write it to the bighcain before retrieving the transaction - block = b.create_block([tx_signed]) + block = b.create_block([tx]) b.write_block(block, durability='hard') - response, status = b.get_transaction(tx_signed["id"], include_status=True) + response, status = b.get_transaction(tx.id, include_status=True) # add validity information, which will be returned - assert util.serialize(tx_signed) == util.serialize(response) + assert tx.to_dict() == response.to_dict() assert status == b.TX_UNDECIDED - @pytest.mark.usefixtures('inputs') - def test_read_transaction_backlog(self, b, user_vk, user_sk): - input_tx = b.get_owned_ids(user_vk).pop() - tx = b.create_transaction(user_vk, user_vk, input_tx, 'TRANSFER') - tx_signed = b.sign_transaction(tx, user_sk) - b.write_transaction(tx_signed) - - response, status = b.get_transaction(tx_signed["id"], include_status=True) - - # add validity information, which will be returned - assert util.serialize(tx_signed) == util.serialize(response) - assert status == b.TX_IN_BACKLOG - @pytest.mark.usefixtures('inputs') def test_read_transaction_invalid_block(self, b, user_vk, user_sk): + from bigchaindb.models import Transaction + input_tx = b.get_owned_ids(user_vk).pop() - tx = b.create_transaction(user_vk, user_vk, input_tx, 'TRANSFER') - tx_signed = b.sign_transaction(tx, user_sk) + input_tx = b.get_transaction(input_tx.txid) + inputs = input_tx.to_inputs() + tx = Transaction.transfer(inputs, [user_vk]) + tx = tx.sign([user_sk]) + b.write_transaction(tx) # create block - block = b.create_block([tx_signed]) + 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) + vote = b.vote(block.id, b.get_last_voted_block().id, False) b.write_vote(vote) - response = b.get_transaction(tx_signed["id"]) + response = b.get_transaction(tx.id) # should be None, because invalid blocks are ignored assert response is None - @pytest.mark.usefixtures('inputs') - def test_read_transaction_valid_block(self, b, user_vk, user_sk): - input_tx = b.get_owned_ids(user_vk).pop() - tx = b.create_transaction(user_vk, user_vk, input_tx, 'TRANSFER') - tx_signed = b.sign_transaction(tx, user_sk) - b.write_transaction(tx_signed) - - # create block - block = b.create_block([tx_signed]) - b.write_block(block, durability='hard') - - # vote the block invalid - vote = b.vote(block['id'], b.get_last_voted_block()['id'], True) - b.write_vote(vote) - - response, status = b.get_transaction(tx_signed["id"], include_status=True) - # add validity information, which will be returned - assert util.serialize(tx_signed) == util.serialize(response) - assert status == b.TX_VALID - - @pytest.mark.usefixtures('inputs') - def test_assign_transaction_one_node(self, b, user_vk, user_sk): - input_tx = b.get_owned_ids(user_vk).pop() - tx = b.create_transaction(user_vk, user_vk, input_tx, 'TRANSFER') - tx_signed = b.sign_transaction(tx, user_sk) - b.write_transaction(tx_signed) - - # retrieve the transaction - response = r.table('backlog').get(tx_signed['id']).run(b.conn) - - # check if the assignee is the current node - assert response['assignee'] == b.me - - @pytest.mark.usefixtures('inputs') - def test_assign_transaction_multiple_nodes(self, b, user_vk, user_sk): - # create 5 federation nodes - for _ in range(5): - b.nodes_except_me.append(crypto.generate_key_pair()[1]) - - # test assignee for several transactions - for _ in range(20): - input_tx = b.get_owned_ids(user_vk).pop() - tx = b.create_transaction(user_vk, user_vk, input_tx, 'TRANSFER') - tx_signed = b.sign_transaction(tx, user_sk) - b.write_transaction(tx_signed) - - # retrieve the transaction - response = r.table('backlog').get(tx_signed['id']).run(b.conn) - - # check if the assignee is one of the _other_ federation nodes - assert response['assignee'] in b.nodes_except_me - @pytest.mark.usefixtures('inputs') def test_genesis_block(self, b): + import rethinkdb as r + from bigchaindb.util import is_genesis_block response = list(r.table('bigchain') - .filter(util.is_genesis_block) + .filter(is_genesis_block) .run(b.conn)) assert len(response) == 1 @@ -219,19 +267,23 @@ class TestBigchainApi(object): assert block['block']['transactions'][0]['transaction']['fulfillments'][0]['input'] is None def test_create_genesis_block_fails_if_table_not_empty(self, b): + import rethinkdb as r + from bigchaindb_common.exceptions import GenesisBlockAlreadyExistsError + from bigchaindb.util import is_genesis_block b.create_genesis_block() - with pytest.raises(exceptions.GenesisBlockAlreadyExistsError): + with pytest.raises(GenesisBlockAlreadyExistsError): b.create_genesis_block() genesis_blocks = list(r.table('bigchain') - .filter(util.is_genesis_block) + .filter(is_genesis_block) .run(b.conn)) assert len(genesis_blocks) == 1 @pytest.mark.skipif(reason='This test may not make sense after changing the chainification mode') def test_get_last_block(self, b): + import rethinkdb as r # get the number of blocks num_blocks = r.table('bigchain').count().run(b.conn) @@ -267,27 +319,24 @@ class TestBigchainApi(object): assert prev_block_id == last_block['id'] - def test_create_new_block(self, b): - tx = dummy_tx() - new_block = b.create_block([tx]) - block_hash = crypto.hash_data(util.serialize(new_block['block'])) - - assert new_block['block']['voters'] == [b.me] - assert new_block['block']['node_pubkey'] == b.me - assert crypto.VerifyingKey(b.me).verify(util.serialize(new_block['block']), new_block['signature']) is True - assert new_block['id'] == block_hash - def test_create_empty_block(self, b): - with pytest.raises(exceptions.OperationError) as excinfo: + from bigchaindb_common.exceptions import OperationError + + with pytest.raises(OperationError) as excinfo: b.create_block([]) assert excinfo.value.args[0] == 'Empty block creation is not allowed' def test_get_last_voted_block_returns_genesis_if_no_votes_has_been_casted(self, b): + import rethinkdb as r + from bigchaindb import util + from bigchaindb.models import Block + b.create_genesis_block() genesis = list(r.table('bigchain') .filter(util.is_genesis_block) .run(b.conn))[0] + genesis = Block.from_dict(genesis) gb = b.get_last_voted_block() assert gb == genesis assert b.validate_block(gb) == gb @@ -297,263 +346,266 @@ class TestBigchainApi(object): assert b.get_last_voted_block() == genesis + monkeypatch.setattr('time.time', lambda: 1) block_1 = dummy_block() + monkeypatch.setattr('time.time', lambda: 2) block_2 = dummy_block() + monkeypatch.setattr('time.time', lambda: 3) block_3 = dummy_block() b.write_block(block_1, durability='hard') b.write_block(block_2, durability='hard') b.write_block(block_3, durability='hard') - # make sure all the blocks are written at the same time - monkeypatch.setattr(util, 'timestamp', lambda: '1') + # make sure all the votes are written with the same timestamps + monkeypatch.setattr('time.time', lambda: 4) + b.write_vote(b.vote(block_1.id, b.get_last_voted_block().id, True)) + assert b.get_last_voted_block().id == block_1.id - b.write_vote(b.vote(block_1['id'], b.get_last_voted_block()['id'], True)) - assert b.get_last_voted_block()['id'] == block_1['id'] - - b.write_vote(b.vote(block_2['id'], b.get_last_voted_block()['id'], True)) - assert b.get_last_voted_block()['id'] == block_2['id'] - - b.write_vote(b.vote(block_3['id'], b.get_last_voted_block()['id'], True)) - assert b.get_last_voted_block()['id'] == block_3['id'] + b.write_vote(b.vote(block_2.id, b.get_last_voted_block().id, True)) + assert b.get_last_voted_block().id == block_2.id + b.write_vote(b.vote(block_3.id, b.get_last_voted_block().id, True)) + assert b.get_last_voted_block().id == block_3.id def test_get_last_voted_block_returns_the_correct_block_different_timestamps(self, b, monkeypatch): genesis = b.create_genesis_block() assert b.get_last_voted_block() == genesis + monkeypatch.setattr('time.time', lambda: 1) block_1 = dummy_block() + monkeypatch.setattr('time.time', lambda: 2) block_2 = dummy_block() + monkeypatch.setattr('time.time', lambda: 3) block_3 = dummy_block() b.write_block(block_1, durability='hard') b.write_block(block_2, durability='hard') b.write_block(block_3, durability='hard') - # make sure all the blocks are written at different timestamps - monkeypatch.setattr(util, 'timestamp', lambda: '1') - b.write_vote(b.vote(block_1['id'], b.get_last_voted_block()['id'], True)) - assert b.get_last_voted_block()['id'] == block_1['id'] + # make sure all the votes are written with different timestamps + monkeypatch.setattr('time.time', lambda: 4) + b.write_vote(b.vote(block_1.id, b.get_last_voted_block().id, True)) + assert b.get_last_voted_block().id == block_1.id - monkeypatch.setattr(util, 'timestamp', lambda: '2') - b.write_vote(b.vote(block_2['id'], b.get_last_voted_block()['id'], True)) - assert b.get_last_voted_block()['id'] == block_2['id'] + monkeypatch.setattr('time.time', lambda: 5) + b.write_vote(b.vote(block_2.id, b.get_last_voted_block().id, True)) + assert b.get_last_voted_block().id == block_2.id - monkeypatch.setattr(util, 'timestamp', lambda: '3') - b.write_vote(b.vote(block_3['id'], b.get_last_voted_block()['id'], True)) - assert b.get_last_voted_block()['id'] == block_3['id'] + monkeypatch.setattr('time.time', lambda: 6) + b.write_vote(b.vote(block_3.id, b.get_last_voted_block().id, True)) + assert b.get_last_voted_block().id == block_3.id def test_no_vote_written_if_block_already_has_vote(self, b): + import rethinkdb as r + from bigchaindb.models import Block + genesis = b.create_genesis_block() - block_1 = dummy_block() - b.write_block(block_1, durability='hard') - b.write_vote(b.vote(block_1['id'], genesis['id'], True)) - retrieved_block_1 = r.table('bigchain').get(block_1['id']).run(b.conn) + b.write_vote(b.vote(block_1.id, genesis.id, True)) + retrieved_block_1 = r.table('bigchain').get(block_1.id).run(b.conn) + retrieved_block_1 = Block.from_dict(retrieved_block_1) # try to vote again on the retrieved block, should do nothing - b.write_vote(b.vote(retrieved_block_1['id'], genesis['id'], True)) - retrieved_block_2 = r.table('bigchain').get(block_1['id']).run(b.conn) + b.write_vote(b.vote(retrieved_block_1.id, genesis.id, True)) + retrieved_block_2 = r.table('bigchain').get(block_1.id).run(b.conn) + retrieved_block_2 = Block.from_dict(retrieved_block_2) assert retrieved_block_1 == retrieved_block_2 def test_more_votes_than_voters(self, b): + import rethinkdb as r + from bigchaindb_common.exceptions import MultipleVotesError + b.create_genesis_block() block_1 = dummy_block() b.write_block(block_1, durability='hard') # insert duplicate votes - vote_1 = b.vote(block_1['id'], b.get_last_voted_block()['id'], True) - vote_2 = b.vote(block_1['id'], b.get_last_voted_block()['id'], True) + vote_1 = b.vote(block_1.id, b.get_last_voted_block().id, True) + vote_2 = b.vote(block_1.id, b.get_last_voted_block().id, True) vote_2['node_pubkey'] = 'aaaaaaa' r.table('votes').insert(vote_1).run(b.conn) r.table('votes').insert(vote_2).run(b.conn) - from bigchaindb.exceptions import MultipleVotesError with pytest.raises(MultipleVotesError) as excinfo: - b.block_election_status(block_1) + b.block_election_status(block_1.id, block_1.voters) assert excinfo.value.args[0] == 'Block {block_id} has {n_votes} votes cast, but only {n_voters} voters'\ - .format(block_id=block_1['id'], n_votes=str(2), n_voters=str(1)) + .format(block_id=block_1.id, n_votes=str(2), n_voters=str(1)) def test_multiple_votes_single_node(self, b): + import rethinkdb as r + from bigchaindb_common.exceptions import MultipleVotesError genesis = b.create_genesis_block() block_1 = dummy_block() b.write_block(block_1, durability='hard') # insert duplicate votes for i in range(2): - r.table('votes').insert(b.vote(block_1['id'], genesis['id'], True)).run(b.conn) + r.table('votes').insert(b.vote(block_1.id, genesis.id, True)).run(b.conn) - from bigchaindb.exceptions import MultipleVotesError with pytest.raises(MultipleVotesError) as excinfo: - b.block_election_status(block_1) + b.block_election_status(block_1.id, block_1.voters) assert excinfo.value.args[0] == 'Block {block_id} has multiple votes ({n_votes}) from voting node {node_id}'\ - .format(block_id=block_1['id'], n_votes=str(2), node_id=b.me) + .format(block_id=block_1.id, n_votes=str(2), node_id=b.me) with pytest.raises(MultipleVotesError) as excinfo: - b.has_previous_vote(block_1) + b.has_previous_vote(block_1.id, block_1.voters) assert excinfo.value.args[0] == 'Block {block_id} has {n_votes} votes from public key {me}'\ - .format(block_id=block_1['id'], n_votes=str(2), me=b.me) + .format(block_id=block_1.id, n_votes=str(2), me=b.me) def test_improper_vote_error(selfs, b): + import rethinkdb as r + from bigchaindb_common.exceptions import ImproperVoteError b.create_genesis_block() block_1 = dummy_block() b.write_block(block_1, durability='hard') - vote_1 = b.vote(block_1['id'], b.get_last_voted_block()['id'], True) + vote_1 = b.vote(block_1.id, b.get_last_voted_block().id, True) # mangle the signature vote_1['signature'] = 'a' * 87 r.table('votes').insert(vote_1).run(b.conn) - from bigchaindb.exceptions import ImproperVoteError with pytest.raises(ImproperVoteError) as excinfo: - b.has_previous_vote(block_1) + b.has_previous_vote(block_1.id, block_1.id) assert excinfo.value.args[0] == 'Block {block_id} already has an incorrectly signed ' \ - 'vote from public key {me}'.format(block_id=block_1['id'], me=b.me) + 'vote from public key {me}'.format(block_id=block_1.id, me=b.me) + + @pytest.mark.usefixtures('inputs') + def test_assign_transaction_one_node(self, b, user_vk, user_sk): + import rethinkdb as r + 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]) + tx = tx.sign([user_sk]) + b.write_transaction(tx) + + # retrieve the transaction + response = r.table('backlog').get(tx.id).run(b.conn) + + # check if the assignee is the current node + assert response['assignee'] == b.me + + @pytest.mark.usefixtures('inputs') + def test_assign_transaction_multiple_nodes(self, b, user_vk, user_sk): + import rethinkdb as r + from bigchaindb_common.crypto import generate_key_pair + from bigchaindb.models import Transaction + + # create 5 federation nodes + for _ in range(5): + b.nodes_except_me.append(generate_key_pair()[1]) + + # test assignee for several transactions + for _ in range(20): + 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]) + tx = tx.sign([user_sk]) + b.write_transaction(tx) + + # retrieve the transaction + response = r.table('backlog').get(tx.id).run(b.conn) + + # check if the assignee is one of the _other_ federation nodes + assert response['assignee'] in b.nodes_except_me class TestTransactionValidation(object): - @pytest.mark.usefixtures('inputs') - def test_create_operation_with_inputs(self, b, user_vk): - input_tx = b.get_owned_ids(user_vk).pop() - tx = b.create_transaction(b.me, user_vk, input_tx, 'CREATE') - with pytest.raises(ValueError) as excinfo: - b.validate_transaction(tx) + def test_create_operation_with_inputs(self, b, user_vk, create_tx): + from bigchaindb_common.transaction import TransactionLink + # Manipulate fulfillment so that it has a `tx_input` defined even + # though it shouldn't have one + create_tx.fulfillments[0].tx_input = TransactionLink('abc', 0) + with pytest.raises(ValueError) as excinfo: + b.validate_transaction(create_tx) assert excinfo.value.args[0] == 'A CREATE operation has no inputs' - assert b.is_valid_transaction(tx) is False - def test_create_operation_not_federation_node(self, b, user_vk): - tx = b.create_transaction(user_vk, user_vk, None, 'CREATE') - with pytest.raises(exceptions.OperationError) as excinfo: - b.validate_transaction(tx) - - assert excinfo.value.args[0] == 'Only federation nodes can use the operation `CREATE`' - assert b.is_valid_transaction(tx) is False - - def test_non_create_operation_no_inputs(self, b, user_vk): - tx = b.create_transaction(user_vk, user_vk, None, 'TRANSFER') + def test_transfer_operation_no_inputs(self, b, user_vk, + signed_transfer_tx): + signed_transfer_tx.fulfillments[0].tx_input = None with pytest.raises(ValueError) as excinfo: - b.validate_transaction(tx) + b.validate_transaction(signed_transfer_tx) assert excinfo.value.args[0] == 'Only `CREATE` transactions can have null inputs' - assert b.is_valid_transaction(tx) is False - def test_non_create_input_not_found(self, b, user_vk): - tx = b.create_transaction(user_vk, user_vk, {'txid': 'c', 'cid': 0}, 'TRANSFER') - with pytest.raises(exceptions.TransactionDoesNotExist) as excinfo: - b.validate_transaction(tx) + def test_non_create_input_not_found(self, b, user_vk, signed_transfer_tx): + from bigchaindb_common.exceptions import TransactionDoesNotExist + from bigchaindb_common.transaction import TransactionLink - assert excinfo.value.args[0] == 'input `c` does not exist in the bigchain' - assert b.is_valid_transaction(tx) is False + signed_transfer_tx.fulfillments[0].tx_input = TransactionLink('c', 0) + with pytest.raises(TransactionDoesNotExist): + b.validate_transaction(signed_transfer_tx) @pytest.mark.usefixtures('inputs') def test_non_create_valid_input_wrong_owner(self, b, user_vk): - input_valid = b.get_owned_ids(user_vk).pop() - sk, vk = crypto.generate_key_pair() - tx = b.create_transaction(vk, user_vk, input_valid, 'TRANSFER') - with pytest.raises(exceptions.InvalidSignature) as excinfo: + from bigchaindb_common.crypto import generate_key_pair + from bigchaindb_common.exceptions import InvalidSignature + from bigchaindb.models import Transaction + + input_tx = b.get_owned_ids(user_vk).pop() + sk, vk = generate_key_pair() + tx = Transaction.create([vk], [user_vk]) + tx.operation = 'TRANSFER' + tx.fulfillments[0].tx_input = input_tx + + with pytest.raises(InvalidSignature): b.validate_transaction(tx) - # assert excinfo.value.args[0] == 'owner_before `a` does not own the input `{}`'.format(valid_input) - assert b.is_valid_transaction(tx) is False + def test_non_create_double_spend(self, b, signed_create_tx, + signed_transfer_tx): + from bigchaindb_common.exceptions import DoubleSpend - @pytest.mark.usefixtures('inputs') - def test_non_create_double_spend(self, b, user_vk, user_sk): - input_valid = b.get_owned_ids(user_vk).pop() - tx_valid = b.create_transaction(user_vk, user_vk, input_valid, 'TRANSFER') - tx_valid_signed = b.sign_transaction(tx_valid, user_sk) - b.write_transaction(tx_valid_signed) + block1 = b.create_block([signed_create_tx]) + b.write_block(block1) - # create and write block to bigchain - block = b.create_block([tx_valid_signed]) + b.write_transaction(signed_transfer_tx) + block = b.create_block([signed_transfer_tx]) b.write_block(block, durability='hard') - # create another transaction with the same input - tx_double_spend = b.create_transaction(user_vk, user_vk, input_valid, 'TRANSFER') - with pytest.raises(exceptions.DoubleSpend) as excinfo: - b.validate_transaction(tx_double_spend) - - assert excinfo.value.args[0] == 'input `{}` was already spent'.format(input_valid) - assert b.is_valid_transaction(tx_double_spend) is False + signed_transfer_tx.timestamp = 123 + # FIXME: https://github.com/bigchaindb/bigchaindb/issues/592 + with pytest.raises(DoubleSpend): + b.validate_transaction(signed_transfer_tx) @pytest.mark.usefixtures('inputs') - def test_wrong_transaction_hash(self, b, user_vk): - input_valid = b.get_owned_ids(user_vk).pop() - tx_valid = b.create_transaction(user_vk, user_vk, input_valid, 'TRANSFER') + def test_valid_non_create_transaction_after_block_creation(self, b, + user_vk, + user_sk): + from bigchaindb.models import Transaction - # change the transaction hash - tx_valid.update({'id': 'abcd'}) - with pytest.raises(exceptions.InvalidHash): - b.validate_transaction(tx_valid) - assert b.is_valid_transaction(tx_valid) is False + 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]) + transfer_tx = transfer_tx.sign([user_sk]) - @pytest.mark.usefixtures('inputs') - def test_wrong_signature(self, b, user_sk, user_vk): - input_valid = b.get_owned_ids(user_vk).pop() - tx_valid = b.create_transaction(user_vk, user_vk, input_valid, 'TRANSFER') - - wrong_private_key = '4fyvJe1aw2qHZ4UNRYftXK7JU7zy9bCqoU5ps6Ne3xrY' - - with pytest.raises(exceptions.KeypairMismatchException): - tx_invalid_signed = b.sign_transaction(tx_valid, wrong_private_key) - - # create a correctly signed transaction and change the signature - tx_signed = b.sign_transaction(tx_valid, user_sk) - fulfillment = tx_signed['transaction']['fulfillments'][0]['fulfillment'] - changed_fulfillment = cc.Ed25519Fulfillment().from_uri(fulfillment) - changed_fulfillment.signature = b'0' * 64 - tx_signed['transaction']['fulfillments'][0]['fulfillment'] = changed_fulfillment.serialize_uri() - - with pytest.raises(exceptions.InvalidSignature): - b.validate_transaction(tx_signed) - assert b.is_valid_transaction(tx_signed) is False - - def test_valid_create_transaction(self, b, user_vk): - tx = b.create_transaction(b.me, user_vk, None, 'CREATE') - tx_signed = b.sign_transaction(tx, b.me_private) - assert tx_signed == b.validate_transaction(tx_signed) - assert tx_signed == b.is_valid_transaction(tx_signed) - - @pytest.mark.usefixtures('inputs') - def test_valid_non_create_transaction(self, b, user_vk, user_sk): - input_valid = b.get_owned_ids(user_vk).pop() - tx_valid = b.create_transaction(user_vk, user_vk, input_valid, 'TRANSFER') - - tx_valid_signed = b.sign_transaction(tx_valid, user_sk) - assert tx_valid_signed == b.validate_transaction(tx_valid_signed) - assert tx_valid_signed == b.is_valid_transaction(tx_valid_signed) - - @pytest.mark.usefixtures('inputs') - def test_valid_non_create_transaction_after_block_creation(self, b, user_vk, user_sk): - input_valid = b.get_owned_ids(user_vk).pop() - tx_valid = b.create_transaction(user_vk, user_vk, input_valid, 'TRANSFER') - - tx_valid_signed = b.sign_transaction(tx_valid, user_sk) - assert tx_valid_signed == b.validate_transaction(tx_valid_signed) - assert tx_valid_signed == b.is_valid_transaction(tx_valid_signed) + assert transfer_tx == b.validate_transaction(transfer_tx) # create block - block = b.create_block([tx_valid_signed]) - assert b.is_valid_block(block) + block = b.create_block([transfer_tx]) + assert b.validate_block(block) == block b.write_block(block, durability='hard') - # check that the transaction is still valid after being written to the bigchain - assert tx_valid_signed == b.validate_transaction(tx_valid_signed) - assert tx_valid_signed == b.is_valid_transaction(tx_valid_signed) + # check that the transaction is still valid after being written to the + # bigchain + assert transfer_tx == b.validate_transaction(transfer_tx) class TestBlockValidation(object): - def test_wrong_block_hash(self, b): - block = dummy_block() - - # change block hash - block.update({'id': 'abc'}) - with pytest.raises(exceptions.InvalidHash): - b.validate_block(block) - @pytest.mark.skipif(reason='Separated tx validation from block creation.') @pytest.mark.usefixtures('inputs') - def test_invalid_transactions_in_block(self, b, user_vk, ): + def test_invalid_transactions_in_block(self, b, user_vk): + from bigchaindb_common import crypto + from bigchaindb_common.exceptions import TransactionOwnerError + from bigchaindb_common.util import gen_timestamp + + from bigchaindb import util + # invalid transaction valid_input = b.get_owned_ids(user_vk).pop() tx_invalid = b.create_transaction('a', 'b', valid_input, 'c') @@ -562,13 +614,15 @@ class TestBlockValidation(object): # create a block with invalid transactions block = { - 'timestamp': util.timestamp(), + 'timestamp': gen_timestamp(), 'transactions': [tx_invalid], 'node_pubkey': b.me, 'voters': b.nodes_except_me } - block_data = util.serialize(block) + # NOTE: This is not the correct function anymore, but this test is + # skipped + block_data = util.serialize_block(block) block_hash = crypto.hash_data(block_data) block_signature = crypto.SigningKey(b.me_private).sign(block_data) @@ -579,44 +633,31 @@ class TestBlockValidation(object): 'votes': [] } - with pytest.raises(exceptions.TransactionOwnerError) as excinfo: + with pytest.raises(TransactionOwnerError) as excinfo: + # TODO: Adjust this to the new Block model (test is currently + # skipped. b.validate_block(block) assert excinfo.value.args[0] == 'owner_before `a` does not own the input `{}`'.format(valid_input) - def test_invalid_block_id(self, b): - block = dummy_block() - - # change block hash - block.update({'id': 'abc'}) - with pytest.raises(exceptions.InvalidHash): - b.validate_block(block) - - @pytest.mark.usefixtures('inputs') - def test_valid_block(self, b, user_vk, user_sk): - # create valid transaction - input_valid = b.get_owned_ids(user_vk).pop() - tx_valid = b.create_transaction(user_vk, user_vk, input_valid, 'TRANSFER') - tx_valid_signed = b.sign_transaction(tx_valid, user_sk) - - # create valid block - block = b.create_block([tx_valid_signed]) - - assert block == b.validate_block(block) - assert b.is_valid_block(block) - def test_invalid_signature(self, b): + from bigchaindb_common.exceptions import InvalidSignature + from bigchaindb_common import crypto + # create a valid block block = dummy_block() # replace the block signature with an invalid one - block['signature'] = crypto.SigningKey(b.me_private).sign(b'wrongdata') + block.signature = crypto.SigningKey(b.me_private).sign(b'wrongdata') # check that validate_block raises an InvalidSignature exception - with pytest.raises(exceptions.InvalidSignature): + with pytest.raises(InvalidSignature): b.validate_block(block) def test_invalid_node_pubkey(self, b): + from bigchaindb_common.exceptions import OperationError + from bigchaindb_common import crypto + # blocks can only be created by a federation node # create a valid block block = dummy_block() @@ -625,87 +666,91 @@ class TestBlockValidation(object): tmp_sk, tmp_vk = crypto.generate_key_pair() # change the block node_pubkey - block['block']['node_pubkey'] = tmp_vk + block.node_pubkey = tmp_vk # just to make sure lets re-hash the block and create a valid signature # from a non federation node - block['id'] = crypto.hash_data(util.serialize(block['block'])) - block['signature'] = crypto.SigningKey(tmp_sk).sign(util.serialize(block['block'])) + block = block.sign(tmp_sk) # check that validate_block raises an OperationError - with pytest.raises(exceptions.OperationError): + with pytest.raises(OperationError): b.validate_block(block) class TestMultipleInputs(object): - def test_transfer_single_owners_single_input(self, b, user_sk, user_vk, inputs): - # create a new user + def test_transfer_single_owner_single_input(self, b, inputs, user_vk, + user_sk): + 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) - inp = owned_inputs.pop() - - # create a transaction - tx = b.create_transaction([user_vk], [user2_sk], inp, 'TRANSFER') - tx_signed = b.sign_transaction(tx, user_sk) + 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]) + tx = tx.sign([user_sk]) # validate transaction - assert b.is_valid_transaction(tx_signed) == tx_signed - assert len(tx_signed['transaction']['fulfillments']) == 1 - assert len(tx_signed['transaction']['conditions']) == 1 + assert b.is_valid_transaction(tx) == tx + assert len(tx.fulfillments) == 1 + assert len(tx.conditions) == 1 def test_transfer_single_owners_multiple_inputs(self, b, user_sk, user_vk): - # create a new user + from bigchaindb_common import crypto + from bigchaindb.models import Transaction + user2_sk, user2_vk = crypto.generate_key_pair() - # create inputs to spend + # TODO: Make this a fixture transactions = [] - for i in range(10): - tx = b.create_transaction(b.me, user_vk, None, 'CREATE') - tx_signed = b.sign_transaction(tx, b.me_private) - transactions.append(tx_signed) - b.write_transaction(tx_signed) + for i in range(3): + tx = Transaction.create([user_vk], [user_vk]) + tx = tx.sign([user_sk]) + transactions.append(tx) + b.write_transaction(tx) block = b.create_block(transactions) b.write_block(block, durability='hard') # get inputs owned_inputs = b.get_owned_ids(user_vk) - inputs = owned_inputs[:3] + 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, 3 * [[user_vk]]) + tx = tx.sign([user_sk]) + assert b.validate_transaction(tx) == tx + assert len(tx.fulfillments) == 3 + assert len(tx.conditions) == 3 - # create a transaction - tx = b.create_transaction(user_vk, user2_vk, inputs, 'TRANSFER') - tx_signed = b.sign_transaction(tx, user_sk) + def test_transfer_single_owners_single_input_from_multiple_outputs(self, b, + user_sk, + user_vk): + import random + from bigchaindb_common import crypto + from bigchaindb.models import Transaction - # validate transaction - assert b.is_valid_transaction(tx_signed) == tx_signed - assert len(tx_signed['transaction']['fulfillments']) == 3 - assert len(tx_signed['transaction']['conditions']) == 3 - - def test_transfer_single_owners_single_input_from_multiple_outputs(self, b, user_sk, user_vk): - # create a new user user2_sk, user2_vk = crypto.generate_key_pair() - # create inputs to spend transactions = [] - for i in range(10): - tx = b.create_transaction(b.me, user_vk, None, 'CREATE') - tx_signed = b.sign_transaction(tx, b.me_private) - transactions.append(tx_signed) - b.write_transaction(tx_signed) + for i in range(3): + payload = {'somedata': random.randint(0, 255)} + tx = Transaction.create([user_vk], [user_vk], None, 'CREATE', payload) + tx = tx.sign([user_sk]) + transactions.append(tx) + b.write_transaction(tx) block = b.create_block(transactions) b.write_block(block, durability='hard') # get inputs owned_inputs = b.get_owned_ids(user_vk) - inputs = owned_inputs[:3] - - # create a transaction - tx = b.create_transaction(user_vk, user2_vk, inputs, 'TRANSFER') - tx_signed = b.sign_transaction(tx, user_sk) + 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, 3 * [[user2_vk]]) + tx = tx.sign([user_sk]) # create block with the transaction - block = b.create_block([tx_signed]) + block = b.create_block([tx]) b.write_block(block, durability='hard') # get inputs from user2 @@ -713,239 +758,262 @@ class TestMultipleInputs(object): assert len(owned_inputs) == 3 # create a transaction with a single input from a multiple output transaction - inp = owned_inputs.pop() - tx = b.create_transaction(user2_vk, user_vk, inp, 'TRANSFER') - tx_signed = b.sign_transaction(tx, user2_sk) + 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_signed) == tx_signed - assert len(tx_signed['transaction']['fulfillments']) == 1 - assert len(tx_signed['transaction']['conditions']) == 1 + 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, + inputs): + from bigchaindb_common import crypto + from bigchaindb.models import Transaction - def test_single_owner_before_multiple_owners_after_single_input(self, b, user_sk, user_vk, inputs): - # create a new users user2_sk, user2_vk = crypto.generate_key_pair() user3_sk, user3_vk = crypto.generate_key_pair() - # get inputs owned_inputs = b.get_owned_ids(user_vk) - inp = owned_inputs.pop() + tx_link = owned_inputs.pop() + inputs = b.get_transaction(tx_link.txid).to_inputs() + tx = Transaction.transfer(inputs, [[user2_vk, user3_vk]]) + tx = tx.sign([user_sk]) - # create a transaction - tx = b.create_transaction(user_vk, [user2_sk, user3_vk], inp, 'TRANSFER') - tx_signed = b.sign_transaction(tx, user_sk) + assert b.is_valid_transaction(tx) == tx + assert len(tx.fulfillments) == 1 + assert len(tx.conditions) == 1 - # validate transaction - assert b.is_valid_transaction(tx_signed) == tx_signed - assert len(tx_signed['transaction']['fulfillments']) == 1 - assert len(tx_signed['transaction']['conditions']) == 1 + def test_single_owner_before_multiple_owners_after_multiple_inputs(self, b, + user_sk, + user_vk): + import random + from bigchaindb_common import crypto + from bigchaindb.models import Transaction - def test_single_owner_before_multiple_owners_after_multiple_inputs(self, b, user_sk, user_vk): - # create a new users user2_sk, user2_vk = crypto.generate_key_pair() user3_sk, user3_vk = crypto.generate_key_pair() - # create inputs to spend transactions = [] - for i in range(5): - tx = b.create_transaction(b.me, user_vk, None, 'CREATE') - tx_signed = b.sign_transaction(tx, b.me_private) - transactions.append(tx_signed) - b.write_transaction(tx_signed) + for i in range(3): + payload = {'somedata': random.randint(0, 255)} + tx = Transaction.create([user_vk], [user_vk], None, 'CREATE', + payload) + tx = tx.sign([user_sk]) + transactions.append(tx) + b.write_transaction(tx) block = b.create_block(transactions) b.write_block(block, durability='hard') - # get inputs owned_inputs = b.get_owned_ids(user_vk) - inputs = owned_inputs[:3] + 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], []) - # create a transaction - tx = b.create_transaction(user_vk, [user2_vk, user3_vk], inputs, 'TRANSFER') - tx_signed = b.sign_transaction(tx, user_sk) + tx = Transaction.transfer(inputs, 3 * [[user2_vk, user3_vk]]) + tx = tx.sign([user_sk]) # validate transaction - assert b.is_valid_transaction(tx_signed) == tx_signed - assert len(tx_signed['transaction']['fulfillments']) == 3 - assert len(tx_signed['transaction']['conditions']) == 3 + assert b.is_valid_transaction(tx) == tx + assert len(tx.fulfillments) == 3 + assert len(tx.conditions) == 3 + + def test_multiple_owners_before_single_owner_after_single_input(self, b, + user_sk, + user_vk): + from bigchaindb_common import crypto + from bigchaindb.models import Transaction - def test_multiple_owners_before_single_owner_after_single_input(self, b, user_sk, user_vk): - # create a new users user2_sk, user2_vk = crypto.generate_key_pair() user3_sk, user3_vk = crypto.generate_key_pair() - # create input to spend - tx = b.create_transaction(b.me, [user_vk, user2_vk], None, 'CREATE') - tx_signed = b.sign_transaction(tx, b.me_private) - block = b.create_block([tx_signed]) + tx = Transaction.create([b.me], [user_vk, user2_vk]) + tx = tx.sign([b.me_private]) + block = b.create_block([tx]) b.write_block(block, durability='hard') - # get input - owned_inputs = b.get_owned_ids(user_vk) - inp = owned_inputs[0] + owned_input = b.get_owned_ids(user_vk).pop() + input_tx = b.get_transaction(owned_input.txid) + inputs = input_tx.to_inputs() - # create a transaction - tx = b.create_transaction([user_vk, user2_vk], user3_vk, inp, 'TRANSFER') - tx_signed = b.sign_transaction(tx, [user_sk, user2_sk]) + transfer_tx = Transaction.transfer(inputs, [user3_vk]) + transfer_tx = transfer_tx.sign([user_sk, user2_sk]) # validate transaction - assert b.is_valid_transaction(tx_signed) == tx_signed - assert len(tx_signed['transaction']['fulfillments']) == 1 - assert len(tx_signed['transaction']['conditions']) == 1 + assert b.is_valid_transaction(transfer_tx) == transfer_tx + assert len(transfer_tx.fulfillments) == 1 + assert len(transfer_tx.conditions) == 1 + + def test_multiple_owners_before_single_owner_after_multiple_inputs(self, b, + user_sk, + user_vk): + from bigchaindb_common import crypto + from bigchaindb.models import Transaction - def test_multiple_owners_before_single_owner_after_multiple_inputs(self, b, user_sk, user_vk): - # create a new users user2_sk, user2_vk = crypto.generate_key_pair() user3_sk, user3_vk = crypto.generate_key_pair() - # create inputs to spend transactions = [] - for i in range(5): - tx = b.create_transaction(b.me, [user_vk, user2_vk], None, 'CREATE') - tx_signed = b.sign_transaction(tx, b.me_private) - transactions.append(tx_signed) + for i in range(3): + tx = Transaction.create([b.me], [user_vk, user2_vk]) + tx = tx.sign([b.me_private]) + transactions.append(tx) block = b.create_block(transactions) b.write_block(block, durability='hard') - # get input - owned_inputs = b.get_owned_ids(user_vk) - inputs = owned_inputs[:3] + tx_links = b.get_owned_ids(user_vk) + inputs = sum([b.get_transaction(tx_link.txid).to_inputs() for tx_link + in tx_links], []) - # create a transaction - tx = b.create_transaction([user_vk, user2_vk], user3_vk, inputs, 'TRANSFER') - tx_signed = b.sign_transaction(tx, [user_sk, user2_sk]) + tx = Transaction.transfer(inputs, len(inputs) * [[user3_vk]]) + tx = tx.sign([user_sk, user2_sk]) - # validate transaction - assert b.is_valid_transaction(tx_signed) == tx_signed - assert len(tx_signed['transaction']['fulfillments']) == 3 - assert len(tx_signed['transaction']['conditions']) == 3 + assert b.is_valid_transaction(tx) == tx + assert len(tx.fulfillments) == 3 + assert len(tx.conditions) == 3 + + def test_multiple_owners_before_multiple_owners_after_single_input(self, b, + user_sk, + user_vk): + from bigchaindb_common import crypto + from bigchaindb.models import Transaction - def test_multiple_owners_before_multiple_owners_after_single_input(self, b, user_sk, user_vk): - # create a new users user2_sk, user2_vk = crypto.generate_key_pair() user3_sk, user3_vk = crypto.generate_key_pair() user4_sk, user4_vk = crypto.generate_key_pair() - # create input to spend - tx = b.create_transaction(b.me, [user_vk, user2_vk], None, 'CREATE') - tx_signed = b.sign_transaction(tx, b.me_private) - block = b.create_block([tx_signed]) + tx = Transaction.create([b.me], [user_vk, user2_vk]) + tx = tx.sign([b.me_private]) + block = b.create_block([tx]) b.write_block(block, durability='hard') - # get input - owned_inputs = b.get_owned_ids(user_vk) - inp = owned_inputs[0] + tx_link = b.get_owned_ids(user_vk).pop() + tx_input = b.get_transaction(tx_link.txid).to_inputs() - # create a transaction - tx = b.create_transaction([user_vk, user2_vk], [user3_vk, user4_vk], inp, 'TRANSFER') - tx_signed = b.sign_transaction(tx, [user_sk, user2_sk]) + tx = Transaction.transfer(tx_input, [[user3_vk, user4_vk]]) + tx = tx.sign([user_sk, user2_sk]) - # validate transaction - assert b.is_valid_transaction(tx_signed) == tx_signed - assert len(tx_signed['transaction']['fulfillments']) == 1 - assert len(tx_signed['transaction']['conditions']) == 1 + assert b.is_valid_transaction(tx) == tx + assert len(tx.fulfillments) == 1 + assert len(tx.conditions) == 1 + + def test_multiple_owners_before_multiple_owners_after_multiple_inputs(self, + b, + user_sk, + user_vk): + from bigchaindb_common import crypto + from bigchaindb.models import Transaction - def test_multiple_owners_before_multiple_owners_after_multiple_inputs(self, b, user_sk, user_vk): - # create a new users user2_sk, user2_vk = crypto.generate_key_pair() user3_sk, user3_vk = crypto.generate_key_pair() user4_sk, user4_vk = crypto.generate_key_pair() - # create inputs to spend transactions = [] - for i in range(5): - tx = b.create_transaction(b.me, [user_vk, user2_vk], None, 'CREATE') - tx_signed = b.sign_transaction(tx, b.me_private) - transactions.append(tx_signed) + for i in range(3): + tx = Transaction.create([b.me], [user_vk, user2_vk]) + tx = tx.sign([b.me_private]) + transactions.append(tx) block = b.create_block(transactions) b.write_block(block, durability='hard') - # get input - owned_inputs = b.get_owned_ids(user_vk) - inp = owned_inputs[:3] + tx_links = b.get_owned_ids(user_vk) + inputs = sum([b.get_transaction(tx_link.txid).to_inputs() for tx_link + in tx_links], []) - # create a transaction - tx = b.create_transaction([user_vk, user2_vk], [user3_vk, user4_vk], inp, 'TRANSFER') - tx_signed = b.sign_transaction(tx, [user_sk, user2_sk]) + tx = Transaction.transfer(inputs, len(inputs) * [[user3_vk, user4_vk]]) + tx = tx.sign([user_sk, user2_sk]) - # validate transaction - assert b.is_valid_transaction(tx_signed) == tx_signed - assert len(tx_signed['transaction']['fulfillments']) == 3 - assert len(tx_signed['transaction']['conditions']) == 3 + assert b.is_valid_transaction(tx) == tx + assert len(tx.fulfillments) == 3 + assert len(tx.conditions) == 3 def test_get_owned_ids_single_tx_single_output(self, b, user_sk, user_vk): - # create a new users + from bigchaindb_common import crypto + from bigchaindb_common.transaction import TransactionLink + from bigchaindb.models import Transaction + user2_sk, user2_vk = crypto.generate_key_pair() - # create input to spend - tx = b.create_transaction(b.me, user_vk, None, 'CREATE') - tx_signed = b.sign_transaction(tx, b.me_private) - block = b.create_block([tx_signed]) + tx = Transaction.create([b.me], [user_vk]) + tx = tx.sign([b.me_private]) + block = b.create_block([tx]) 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) - assert owned_inputs_user1 == [{'cid': 0, 'txid': tx['id']}] + assert owned_inputs_user1 == [TransactionLink(tx.id, 0)] assert owned_inputs_user2 == [] - # create a transaction and block - tx = b.create_transaction(user_vk, user2_vk, owned_inputs_user1, 'TRANSFER') - tx_signed = b.sign_transaction(tx, user_sk) - block = b.create_block([tx_signed]) + tx = Transaction.transfer(tx.to_inputs(), [user2_vk]) + tx = tx.sign([user_sk]) + block = b.create_block([tx]) 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 == [{'cid': 0, 'txid': tx['id']}] + assert owned_inputs_user2 == [TransactionLink(tx.id, 0)] + + def test_get_owned_ids_single_tx_single_output_invalid_block(self, b, + user_sk, + user_vk): + from bigchaindb_common import crypto + from bigchaindb_common.transaction import TransactionLink + from bigchaindb.models import Transaction - def test_get_owned_ids_single_tx_single_output_invalid_block(self, b, user_sk, user_vk): genesis = b.create_genesis_block() - # create a new users user2_sk, user2_vk = crypto.generate_key_pair() - # create input to spend - tx = b.create_transaction(b.me, user_vk, None, 'CREATE') - tx_signed = b.sign_transaction(tx, b.me_private) - block = b.create_block([tx_signed]) + tx = Transaction.create([b.me], [user_vk]) + tx = tx.sign([b.me_private]) + block = b.create_block([tx]) b.write_block(block, durability='hard') # vote the block VALID - vote = b.vote(block['id'], genesis['id'], True) + vote = b.vote(block.id, genesis.id, True) b.write_vote(vote) - # get input owned_inputs_user1 = b.get_owned_ids(user_vk) owned_inputs_user2 = b.get_owned_ids(user2_vk) - assert owned_inputs_user1 == [{'cid': 0, 'txid': tx['id']}] + assert owned_inputs_user1 == [TransactionLink(tx.id, 0)] assert owned_inputs_user2 == [] - # create a transaction and block - tx_invalid = b.create_transaction(user_vk, user2_vk, owned_inputs_user1, 'TRANSFER') - tx_invalid_signed = b.sign_transaction(tx_invalid, user_sk) - block = b.create_block([tx_invalid_signed]) + # 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_invalid = tx_invalid.sign([user_sk]) + block = b.create_block([tx_invalid]) b.write_block(block, durability='hard') # vote the block invalid - vote = b.vote(block['id'], b.get_last_voted_block()['id'], False) + vote = b.vote(block.id, b.get_last_voted_block().id, False) b.write_vote(vote) owned_inputs_user1 = b.get_owned_ids(user_vk) owned_inputs_user2 = b.get_owned_ids(user2_vk) # should be the same as before (note tx, not tx_invalid) - assert owned_inputs_user1 == [{'cid': 0, 'txid': tx['id']}] + assert owned_inputs_user1 == [TransactionLink(tx.id, 0)] assert owned_inputs_user2 == [] - def test_get_owned_ids_single_tx_multiple_outputs(self, b, user_sk, user_vk): - # create a new users + 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.models import Transaction + user2_sk, user2_vk = crypto.generate_key_pair() - # create inputs to spend transactions = [] - for i in range(5): - tx = b.create_transaction(b.me, user_vk, None, 'CREATE') - tx_signed = b.sign_transaction(tx, b.me_private) - transactions.append(tx_signed) + 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) b.write_block(block, durability='hard') @@ -953,1136 +1021,195 @@ class TestMultipleInputs(object): owned_inputs_user1 = b.get_owned_ids(user_vk) owned_inputs_user2 = b.get_owned_ids(user2_vk) - expected_owned_inputs_user1 = [{'txid': tx['id'], 'cid': 0} for tx in transactions] + expected_owned_inputs_user1 = [TransactionLink(tx.id, 0) for tx + in transactions] assert owned_inputs_user1 == expected_owned_inputs_user1 assert owned_inputs_user2 == [] - # create a transaction and block - tx = b.create_transaction(user_vk, user2_vk, - [expected_owned_inputs_user1.pop(), expected_owned_inputs_user1.pop()], 'TRANSFER') - tx_signed = b.sign_transaction(tx, user_sk) - block = b.create_block([tx_signed]) + 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]) 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 == expected_owned_inputs_user1 - assert owned_inputs_user2 == [{'cid': 0, 'txid': tx['id']}, {'cid': 1, 'txid': tx['id']}] + assert owned_inputs_user1 == [] + assert owned_inputs_user2 == [TransactionLink(tx.id, 0), + TransactionLink(tx.id, 1)] def test_get_owned_ids_multiple_owners(self, b, user_sk, user_vk): - # create a new users + from bigchaindb_common import crypto + from bigchaindb_common.transaction import TransactionLink + from bigchaindb.models import Transaction + user2_sk, user2_vk = crypto.generate_key_pair() user3_sk, user3_vk = crypto.generate_key_pair() - # create inputs to spend - transactions = [] - for i in range(5): - tx = b.create_transaction(b.me, [user_vk, user2_vk], None, 'CREATE') - tx_signed = b.sign_transaction(tx, b.me_private) - transactions.append(tx_signed) - block = b.create_block(transactions) + tx = Transaction.create([b.me], [user_vk, user2_vk]) + tx = tx.sign([b.me_private]) + block = b.create_block([tx]) 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 = [{'txid': tx['id'], 'cid': 0} for tx in transactions] + expected_owned_inputs_user1 = [TransactionLink(tx.id, 0)] + assert owned_inputs_user1 == owned_inputs_user2 assert owned_inputs_user1 == expected_owned_inputs_user1 - # create a transaction - tx = b.create_transaction([user_vk, user2_vk], user3_vk, expected_owned_inputs_user1.pop(), 'TRANSFER') - tx_signed = b.sign_transaction(tx, [user_sk, user2_sk]) - block = b.create_block([tx_signed]) + tx = Transaction.transfer(tx.to_inputs(), [user3_vk]) + tx = tx.sign([user_sk, user2_sk]) + block = b.create_block([tx]) 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 == owned_inputs_user2 - assert owned_inputs_user1 == expected_owned_inputs_user1 + assert owned_inputs_user1 == [] def test_get_spent_single_tx_single_output(self, b, user_sk, user_vk): - # create a new users + from bigchaindb_common import crypto + from bigchaindb.models import Transaction + user2_sk, user2_vk = crypto.generate_key_pair() - # create input to spend - tx = b.create_transaction(b.me, user_vk, None, 'CREATE') - tx_signed = b.sign_transaction(tx, b.me_private) - block = b.create_block([tx_signed]) + tx = Transaction.create([b.me], [user_vk]) + tx = tx.sign([b.me_private]) + block = b.create_block([tx]) b.write_block(block, durability='hard') - # get input - owned_inputs_user1 = b.get_owned_ids(user_vk) + owned_inputs_user1 = b.get_owned_ids(user_vk).pop() # check spents - spent_inputs_user1 = b.get_spent(owned_inputs_user1[0]) + input_txid = owned_inputs_user1.txid + input_cid = owned_inputs_user1.cid + spent_inputs_user1 = b.get_spent(input_txid, input_cid) assert spent_inputs_user1 is None # create a transaction and block - tx = b.create_transaction(user_vk, user2_vk, owned_inputs_user1, 'TRANSFER') - tx_signed = b.sign_transaction(tx, user_sk) - block = b.create_block([tx_signed]) + tx = Transaction.transfer(tx.to_inputs(), [user2_vk]) + tx = tx.sign([user_sk]) + block = b.create_block([tx]) b.write_block(block, durability='hard') - spent_inputs_user1 = b.get_spent(owned_inputs_user1[0]) - assert spent_inputs_user1 == tx_signed + spent_inputs_user1 = b.get_spent(input_txid, input_cid) + assert spent_inputs_user1 == tx def test_get_spent_single_tx_single_output_invalid_block(self, b, user_sk, user_vk): + from bigchaindb_common import crypto + from bigchaindb.models import Transaction + genesis = b.create_genesis_block() # create a new users user2_sk, user2_vk = crypto.generate_key_pair() - # create input to spend - tx = b.create_transaction(b.me, user_vk, None, 'CREATE') - tx_signed = b.sign_transaction(tx, b.me_private) - block = b.create_block([tx_signed]) + tx = Transaction.create([b.me], [user_vk]) + tx = tx.sign([b.me_private]) + block = b.create_block([tx]) b.write_block(block, durability='hard') # vote the block VALID - vote = b.vote(block['id'], genesis['id'], True) + vote = b.vote(block.id, genesis.id, True) b.write_vote(vote) - # get input - owned_inputs_user1 = b.get_owned_ids(user_vk) + owned_inputs_user1 = b.get_owned_ids(user_vk).pop() # check spents - spent_inputs_user1 = b.get_spent(owned_inputs_user1[0]) + input_txid = owned_inputs_user1.txid + input_cid = owned_inputs_user1.cid + spent_inputs_user1 = b.get_spent(input_txid, input_cid) assert spent_inputs_user1 is None # create a transaction and block - tx = b.create_transaction(user_vk, user2_vk, owned_inputs_user1, 'TRANSFER') - tx_signed = b.sign_transaction(tx, user_sk) - block = b.create_block([tx_signed]) + tx = Transaction.transfer(tx.to_inputs(), [user2_vk]) + tx = tx.sign([user_sk]) + 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) + vote = b.vote(block.id, b.get_last_voted_block().id, False) b.write_vote(vote) - response = b.get_transaction(tx_signed["id"]) - spent_inputs_user1 = b.get_spent(owned_inputs_user1[0]) + # NOTE: I have no idea why this line is here + b.get_transaction(tx.id) + spent_inputs_user1 = b.get_spent(input_txid, input_cid) # Now there should be no spents (the block is invalid) assert spent_inputs_user1 is None def test_get_spent_single_tx_multiple_outputs(self, b, user_sk, user_vk): + import random + from bigchaindb_common import crypto + from bigchaindb.models import Transaction + # create a new users user2_sk, user2_vk = crypto.generate_key_pair() - # create inputs to spend transactions = [] - for i in range(5): - tx = b.create_transaction(b.me, user_vk, None, 'CREATE') - tx_signed = b.sign_transaction(tx, b.me_private) - transactions.append(tx_signed) + 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) b.write_block(block, durability='hard') - # get input owned_inputs_user1 = b.get_owned_ids(user_vk) # check spents - for inp in owned_inputs_user1: - assert b.get_spent(inp) is None + for input_tx in owned_inputs_user1: + assert b.get_spent(input_tx.txid, input_tx.cid) is None # select inputs to use - inputs = [owned_inputs_user1.pop(), owned_inputs_user1.pop()] + inputs = sum([tx.to_inputs() for tx in transactions[:2]], []) # create a transaction and block - tx = b.create_transaction(user_vk, user2_vk, inputs, 'TRANSFER') - tx_signed = b.sign_transaction(tx, user_sk) - block = b.create_block([tx_signed]) + tx = Transaction.transfer(inputs, len(inputs) * [[user2_vk]]) + tx = tx.sign([user_sk]) + block = b.create_block([tx]) b.write_block(block, durability='hard') # check that used inputs are marked as spent - for inp in inputs: - assert b.get_spent(inp) == tx_signed + for ffill in inputs: + assert b.get_spent(ffill.tx_input.txid, ffill.tx_input.cid) == tx - # check that the other remain marked as unspent - for inp in owned_inputs_user1: - assert b.get_spent(inp) is None + # check if remaining transaction that was unspent is also perceived + # spendable by BigchainDB + assert b.get_spent(transactions[2].id, 0) is None def test_get_spent_multiple_owners(self, b, user_sk, user_vk): - # create a new users + import random + 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() - # create inputs to spend transactions = [] - for i in range(5): - tx = b.create_transaction(b.me, [user_vk, user2_vk], None, 'CREATE') - tx_signed = b.sign_transaction(tx, b.me_private) - transactions.append(tx_signed) + for i in range(3): + payload = {'somedata': random.randint(0, 255)} + tx = Transaction.create([b.me], [user_vk, user2_vk], payload) + tx = tx.sign([b.me_private]) + transactions.append(tx) block = b.create_block(transactions) b.write_block(block, durability='hard') - # get input owned_inputs_user1 = b.get_owned_ids(user_vk) # check spents - for inp in owned_inputs_user1: - assert b.get_spent(inp) is None - - # select inputs to use - inputs = [owned_inputs_user1.pop()] + for input_tx in owned_inputs_user1: + assert b.get_spent(input_tx.txid, input_tx.cid) is None # create a transaction - tx = b.create_transaction([user_vk, user2_vk], user3_vk, inputs, 'TRANSFER') - tx_signed = b.sign_transaction(tx, [user_sk, user2_sk]) - block = b.create_block([tx_signed]) + tx = Transaction.transfer(transactions[0].to_inputs(), [user3_vk]) + tx = tx.sign([user_sk, user2_sk]) + block = b.create_block([tx]) b.write_block(block, durability='hard') # check that used inputs are marked as spent - for inp in inputs: - assert b.get_spent(inp) == tx_signed + assert b.get_spent(transactions[0].id, 0) == tx # check that the other remain marked as unspent - for inp in owned_inputs_user1: - assert b.get_spent(inp) is None - - -class TestFulfillmentMessage(object): - def test_fulfillment_message_create(self, b, user_vk): - tx = b.create_transaction(b.me, user_vk, None, 'CREATE', payload={'pay': 'load'}) - original_fulfillment = tx['transaction']['fulfillments'][0] - fulfillment_message = util.get_fulfillment_message(tx, original_fulfillment) - - assert sorted(fulfillment_message) == \ - ['condition', 'data', 'fulfillment', 'id', 'operation', 'timestamp', 'version'] - - assert fulfillment_message['data']['payload'] == tx['transaction']['data']['payload'] - assert fulfillment_message['id'] == tx['id'] - assert fulfillment_message['condition'] == tx['transaction']['conditions'][0] - assert fulfillment_message['fulfillment']['owners_before'] == original_fulfillment['owners_before'] - assert fulfillment_message['fulfillment']['fid'] == original_fulfillment['fid'] - assert fulfillment_message['fulfillment']['input'] == original_fulfillment['input'] - assert fulfillment_message['operation'] == tx['transaction']['operation'] - assert fulfillment_message['timestamp'] == tx['transaction']['timestamp'] - assert fulfillment_message['version'] == tx['transaction']['version'] - - @pytest.mark.usefixtures('inputs') - def test_fulfillment_message_transfer(self, b, user_vk): - input_tx = b.get_owned_ids(user_vk).pop() - assert b.validate_fulfillments(b.get_transaction(input_tx['txid'])) == True - - tx = b.create_transaction(user_vk, b.me, input_tx, 'TRANSFER', payload={'pay': 'load'}) - - original_fulfillment = tx['transaction']['fulfillments'][0] - fulfillment_message = util.get_fulfillment_message(tx, original_fulfillment) - - assert sorted(fulfillment_message) == \ - ['condition', 'data', 'fulfillment', 'id', 'operation', 'timestamp', 'version'] - - assert fulfillment_message['data']['payload'] == tx['transaction']['data']['payload'] - assert fulfillment_message['id'] == tx['id'] - assert fulfillment_message['condition'] == tx['transaction']['conditions'][0] - assert fulfillment_message['fulfillment']['owners_before'] == original_fulfillment['owners_before'] - assert fulfillment_message['fulfillment']['fid'] == original_fulfillment['fid'] - assert fulfillment_message['fulfillment']['input'] == original_fulfillment['input'] - assert fulfillment_message['operation'] == tx['transaction']['operation'] - assert fulfillment_message['timestamp'] == tx['transaction']['timestamp'] - assert fulfillment_message['version'] == tx['transaction']['version'] - - def test_fulfillment_message_multiple_owners_before_multiple_owners_after_multiple_inputs(self, b, user_vk): - # create a new users - user2_sk, user2_vk = crypto.generate_key_pair() - user3_sk, user3_vk = crypto.generate_key_pair() - user4_sk, user4_vk = crypto.generate_key_pair() - - # create inputs to spend - transactions = [] - for i in range(5): - tx = b.create_transaction(b.me, [user_vk, user2_vk], None, 'CREATE') - tx_signed = b.sign_transaction(tx, b.me_private) - transactions.append(tx_signed) - block = b.create_block(transactions) - b.write_block(block, durability='hard') - - # get input - owned_inputs = b.get_owned_ids(user_vk) - inp = owned_inputs[:3] - - # create a transaction - tx = b.create_transaction([user_vk, user2_vk], [user3_vk, user4_vk], inp, 'TRANSFER', payload={'pay': 'load'}) - - for original_fulfillment in tx['transaction']['fulfillments']: - fulfillment_message = util.get_fulfillment_message(tx, original_fulfillment) - - assert sorted(fulfillment_message) == \ - ['condition', 'data', 'fulfillment', 'id', 'operation', 'timestamp', 'version'] - - assert fulfillment_message['data']['payload'] == tx['transaction']['data']['payload'] - assert fulfillment_message['id'] == tx['id'] - assert fulfillment_message['condition'] == tx['transaction']['conditions'][original_fulfillment['fid']] - assert fulfillment_message['fulfillment']['owners_before'] == original_fulfillment['owners_before'] - assert fulfillment_message['fulfillment']['fid'] == original_fulfillment['fid'] - assert fulfillment_message['fulfillment']['input'] == original_fulfillment['input'] - assert fulfillment_message['operation'] == tx['transaction']['operation'] - assert fulfillment_message['timestamp'] == tx['transaction']['timestamp'] - assert fulfillment_message['version'] == tx['transaction']['version'] - - -class TestTransactionMalleability(object): - @pytest.mark.usefixtures('inputs') - def test_create_transaction_transfer(self, b, user_vk, user_sk): - input_tx = b.get_owned_ids(user_vk).pop() - assert b.validate_fulfillments(b.get_transaction(input_tx['txid'])) is True - - tx = b.create_transaction(user_vk, b.me, input_tx, 'TRANSFER') - - tx_signed = b.sign_transaction(tx, user_sk) - - assert b.validate_fulfillments(tx_signed) is True - assert b.is_valid_transaction(tx_signed) == tx_signed - - tx_changed = copy.deepcopy(tx_signed) - tx_changed['id'] = 'dsdasd' - assert b.validate_fulfillments(tx_changed) is False - assert b.is_valid_transaction(tx_changed) is False - - tx_changed = copy.deepcopy(tx_signed) - tx_changed['transaction']['version'] = '0' - assert b.validate_fulfillments(tx_changed) is False - assert b.is_valid_transaction(tx_changed) is False - - tx_changed = copy.deepcopy(tx_signed) - tx_changed['transaction']['operation'] = 'CREATE' - assert b.validate_fulfillments(tx_changed) is False - assert b.is_valid_transaction(tx_changed) is False - - tx_changed = copy.deepcopy(tx_signed) - tx_changed['transaction']['timestamp'] = '1463033192.123456' - assert b.validate_fulfillments(tx_changed) is False - assert b.is_valid_transaction(tx_changed) is False - - tx_changed = copy.deepcopy(tx_signed) - tx_changed['transaction']['data'] = { - "hash": "872fa6e6f46246cd44afdb2ee9cfae0e72885fb0910e2bcf9a5a2a4eadb417b8", - "payload": { - "msg": "Hello BigchainDB!" - } - } - assert b.validate_fulfillments(tx_changed) == False - - tx_changed = copy.deepcopy(tx_signed) - tx_changed['transaction']['fulfillments'] = [ - { - "owners_before": [ - "AFbofwJYEB7Cx2fgrPrCJzbdDVRzRKysoGXt4DsvuTGN" - ], - "fid": 0, - "fulfillment": "cf:4:iXaq3UbandDj4DgBhFDcfHjkm2639RwgLmwAHUmuDFMfMEKMZ71eQw2qCMK951kBaNNJel_FCDuYnacn_MsWzYXOUJs6DGW3lYfXI_d55xuqpH2BenvRWKNp98tRRr4B", - "input": None - } - ] - assert b.validate_fulfillments(tx_changed) is False - assert b.is_valid_transaction(tx_changed) is False - - tx_changed = copy.deepcopy(tx_signed) - tx_changed['transaction']['fulfillments'][0]['fid'] = 1 - with pytest.raises(IndexError): - assert b.validate_fulfillments(tx_changed) is False - assert b.is_valid_transaction(tx_changed) is False - - tx_changed = copy.deepcopy(tx_signed) - tx_changed['transaction']['fulfillments'][0]['owners_before'] = [ - "AFbofwJYEB7Cx2fgrPrCJzbdDVRzRKysoGXt4DsvuTGN"] - assert b.validate_fulfillments(tx_changed) is False - assert b.is_valid_transaction(tx_changed) is False - - tx_changed = copy.deepcopy(tx_signed) - tx_changed['transaction']['fulfillments'][0]['input'] = { - "cid": 0, - "txid": "3055348675fc6f23b75f13c55db6d112b66eee068e99d30a802883d3b1784203" - } - with pytest.raises(TypeError): - assert b.validate_fulfillments(tx_changed) is False - assert b.is_valid_transaction(tx_changed) is False - - -class TestCryptoconditions(object): - def test_fulfillment_transaction_create(self, b, user_vk): - tx = b.create_transaction(b.me, user_vk, None, 'CREATE') - condition = tx['transaction']['conditions'][0]['condition'] - condition_from_uri = cc.Condition.from_uri(condition['uri']) - condition_from_dict = cc.Fulfillment.from_dict(condition['details']).condition - - assert condition_from_uri.serialize_uri() == condition_from_dict.serialize_uri() - assert condition['details']['public_key'] == user_vk - - tx_signed = b.sign_transaction(tx, b.me_private) - fulfillment = tx_signed['transaction']['fulfillments'][0] - fulfillment_from_uri = cc.Fulfillment.from_uri(fulfillment['fulfillment']) - - assert fulfillment['owners_before'][0] == b.me - assert fulfillment_from_uri.public_key.to_ascii().decode() == b.me - assert b.validate_fulfillments(tx_signed) == True - assert b.is_valid_transaction(tx_signed) == tx_signed - - @pytest.mark.usefixtures('inputs') - def test_fulfillment_transaction_transfer(self, b, user_vk, user_sk): - # create valid transaction - other_sk, other_vk = crypto.generate_key_pair() - prev_tx_id = b.get_owned_ids(user_vk).pop() - tx = b.create_transaction(user_vk, other_vk, prev_tx_id, 'TRANSFER') - - prev_tx = b.get_transaction(prev_tx_id['txid']) - prev_condition = prev_tx['transaction']['conditions'][0]['condition'] - prev_condition_from_uri = cc.Condition.from_uri(prev_condition['uri']) - prev_condition_from_dict = cc.Fulfillment.from_dict(prev_condition['details']).condition - - assert prev_condition_from_uri.serialize_uri() == prev_condition_from_dict.serialize_uri() - assert prev_condition['details']['public_key'] == user_vk - - condition = tx['transaction']['conditions'][0]['condition'] - condition_from_uri = cc.Condition.from_uri(condition['uri']) - condition_from_dict = cc.Fulfillment.from_dict(condition['details']).condition - - assert condition_from_uri.serialize_uri() == condition_from_dict.serialize_uri() - assert condition['details']['public_key'] == other_vk - - tx_signed = b.sign_transaction(tx, user_sk) - fulfillment = tx_signed['transaction']['fulfillments'][0] - fulfillment_from_uri = cc.Fulfillment.from_uri(fulfillment['fulfillment']) - - assert fulfillment['owners_before'][0] == user_vk - assert fulfillment_from_uri.public_key.to_ascii().decode() == user_vk - assert fulfillment_from_uri.condition.serialize_uri() == prev_condition['uri'] - assert b.validate_fulfillments(tx_signed) == True - assert b.is_valid_transaction(tx_signed) == tx_signed - - def test_override_condition_create(self, b, user_vk): - tx = b.create_transaction(b.me, user_vk, None, 'CREATE') - fulfillment = cc.Ed25519Fulfillment(public_key=user_vk) - tx['transaction']['conditions'][0]['condition'] = { - 'details': fulfillment.to_dict(), - 'uri': fulfillment.condition.serialize_uri() - } - - tx_signed = b.sign_transaction(tx, b.me_private) - - fulfillment = tx_signed['transaction']['fulfillments'][0] - fulfillment_from_uri = cc.Fulfillment.from_uri(fulfillment['fulfillment']) - - assert fulfillment['owners_before'][0] == b.me - assert fulfillment_from_uri.public_key.to_ascii().decode() == b.me - assert b.validate_fulfillments(tx_signed) == True - assert b.is_valid_transaction(tx_signed) == tx_signed - - @pytest.mark.usefixtures('inputs') - def test_override_condition_transfer(self, b, user_vk, user_sk): - # create valid transaction - other_sk, other_vk = crypto.generate_key_pair() - prev_tx_id = b.get_owned_ids(user_vk).pop() - tx = b.create_transaction(user_vk, other_vk, prev_tx_id, 'TRANSFER') - - fulfillment = cc.Ed25519Fulfillment(public_key=other_vk) - tx['transaction']['conditions'][0]['condition'] = { - 'details': fulfillment.to_dict(), - 'uri': fulfillment.condition.serialize_uri() - } - - tx_signed = b.sign_transaction(tx, user_sk) - fulfillment = tx_signed['transaction']['fulfillments'][0] - fulfillment_from_uri = cc.Fulfillment.from_uri(fulfillment['fulfillment']) - - assert fulfillment['owners_before'][0] == user_vk - assert fulfillment_from_uri.public_key.to_ascii().decode() == user_vk - assert b.validate_fulfillments(tx_signed) == True - assert b.is_valid_transaction(tx_signed) == tx_signed - - def test_override_fulfillment_create(self, b, user_vk): - tx = b.create_transaction(b.me, user_vk, None, 'CREATE') - original_fulfillment = tx['transaction']['fulfillments'][0] - fulfillment_message = util.get_fulfillment_message(tx, original_fulfillment, serialized=True) - fulfillment = cc.Ed25519Fulfillment(public_key=b.me) - fulfillment.sign(fulfillment_message, crypto.SigningKey(b.me_private)) - - tx['transaction']['fulfillments'][0]['fulfillment'] = fulfillment.serialize_uri() - - assert b.validate_fulfillments(tx) == True - assert b.is_valid_transaction(tx) == tx - - @pytest.mark.usefixtures('inputs') - def test_override_fulfillment_transfer(self, b, user_vk, user_sk): - # create valid transaction - other_sk, other_vk = crypto.generate_key_pair() - prev_tx_id = b.get_owned_ids(user_vk).pop() - tx = b.create_transaction(user_vk, other_vk, prev_tx_id, 'TRANSFER') - - original_fulfillment = tx['transaction']['fulfillments'][0] - fulfillment_message = util.get_fulfillment_message(tx, original_fulfillment, serialized=True) - fulfillment = cc.Ed25519Fulfillment(public_key=user_vk) - fulfillment.sign(fulfillment_message, crypto.SigningKey(user_sk)) - - tx['transaction']['fulfillments'][0]['fulfillment'] = fulfillment.serialize_uri() - - assert b.validate_fulfillments(tx) == True - assert b.is_valid_transaction(tx) == tx - - @pytest.mark.usefixtures('inputs') - def test_override_condition_and_fulfillment_transfer(self, b, user_vk, user_sk): - other_sk, other_vk = crypto.generate_key_pair() - first_input_tx = b.get_owned_ids(user_vk).pop() - first_tx = b.create_transaction(user_vk, other_vk, first_input_tx, 'TRANSFER') - - first_tx_condition = cc.Ed25519Fulfillment(public_key=other_vk) - first_tx['transaction']['conditions'][0]['condition'] = { - 'details': first_tx_condition.to_dict(), - 'uri': first_tx_condition.condition.serialize_uri() - } - - first_tx_fulfillment = first_tx['transaction']['fulfillments'][0] - first_tx_fulfillment_message = util.get_fulfillment_message(first_tx, first_tx_fulfillment, serialized=True) - first_tx_fulfillment = cc.Ed25519Fulfillment(public_key=user_vk) - first_tx_fulfillment.sign(first_tx_fulfillment_message, crypto.SigningKey(user_sk)) - first_tx['transaction']['fulfillments'][0]['fulfillment'] = first_tx_fulfillment.serialize_uri() - - assert b.validate_transaction(first_tx) == first_tx - assert b.is_valid_transaction(first_tx) == first_tx - - b.write_transaction(first_tx) - - # create and write block to bigchain - block = b.create_block([first_tx]) - b.write_block(block, durability='hard') - - next_input_tx = b.get_owned_ids(other_vk).pop() - # create another transaction with the same input - next_tx = b.create_transaction(other_vk, user_vk, next_input_tx, 'TRANSFER') - - next_tx_fulfillment = next_tx['transaction']['fulfillments'][0] - next_tx_fulfillment_message = util.get_fulfillment_message(next_tx, next_tx_fulfillment, serialized=True) - next_tx_fulfillment = cc.Ed25519Fulfillment(public_key=other_vk) - next_tx_fulfillment.sign(next_tx_fulfillment_message, crypto.SigningKey(other_sk)) - next_tx['transaction']['fulfillments'][0]['fulfillment'] = next_tx_fulfillment.serialize_uri() - - assert b.validate_transaction(next_tx) == next_tx - assert b.is_valid_transaction(next_tx) == next_tx - - @pytest.mark.usefixtures('inputs') - def test_override_condition_and_fulfillment_transfer_threshold(self, b, user_vk, user_sk): - other1_sk, other1_vk = crypto.generate_key_pair() - other2_sk, other2_vk = crypto.generate_key_pair() - other3_sk, other3_vk = crypto.generate_key_pair() - - first_input_tx = b.get_owned_ids(user_vk).pop() - first_tx = b.create_transaction(user_vk, [other1_vk, other2_vk, other3_vk], first_input_tx, 'TRANSFER') - - first_tx_condition = cc.ThresholdSha256Fulfillment(threshold=2) - first_tx_condition.add_subfulfillment(cc.Ed25519Fulfillment(public_key=other1_vk)) - first_tx_condition.add_subfulfillment(cc.Ed25519Fulfillment(public_key=other2_vk)) - first_tx_condition.add_subfulfillment(cc.Ed25519Fulfillment(public_key=other3_vk)) - - first_tx['transaction']['conditions'][0]['condition'] = { - 'details': first_tx_condition.to_dict(), - 'uri': first_tx_condition.condition.serialize_uri() - } - # conditions have been updated, so hash needs updating - first_tx['id'] = util.get_hash_data(first_tx) - - first_tx_signed = b.sign_transaction(first_tx, user_sk) - - assert b.validate_transaction(first_tx_signed) == first_tx_signed - assert b.is_valid_transaction(first_tx_signed) == first_tx_signed - - b.write_transaction(first_tx_signed) - - # create and write block to bigchain - block = b.create_block([first_tx]) - b.write_block(block, durability='hard') - - next_input_tx = b.get_owned_ids(other1_vk).pop() - # create another transaction with the same input - next_tx = b.create_transaction([other1_vk, other2_vk, other3_vk], user_vk, next_input_tx, 'TRANSFER') - - next_tx_fulfillment = next_tx['transaction']['fulfillments'][0] - next_tx_fulfillment_message = util.get_fulfillment_message(next_tx, next_tx_fulfillment, serialized=True) - next_tx_fulfillment = cc.ThresholdSha256Fulfillment(threshold=2) - next_tx_subfulfillment1 = cc.Ed25519Fulfillment(public_key=other1_vk) - next_tx_subfulfillment1.sign(next_tx_fulfillment_message, crypto.SigningKey(other1_sk)) - next_tx_fulfillment.add_subfulfillment(next_tx_subfulfillment1) - next_tx_subfulfillment2 = cc.Ed25519Fulfillment(public_key=other2_vk) - next_tx_subfulfillment2.sign(next_tx_fulfillment_message, crypto.SigningKey(other2_sk)) - next_tx_fulfillment.add_subfulfillment(next_tx_subfulfillment2) - # need to add remaining (unsigned) fulfillment as a condition - next_tx_subfulfillment3 = cc.Ed25519Fulfillment(public_key=other3_vk) - next_tx_fulfillment.add_subcondition(next_tx_subfulfillment3.condition) - next_tx['transaction']['fulfillments'][0]['fulfillment'] = next_tx_fulfillment.serialize_uri() - - assert b.validate_transaction(next_tx) == next_tx - assert b.is_valid_transaction(next_tx) == next_tx - - @pytest.mark.usefixtures('inputs') - def test_override_condition_and_fulfillment_transfer_threshold_from_dict(self, b, user_vk, user_sk): - other1_sk, other1_vk = crypto.generate_key_pair() - other2_sk, other2_vk = crypto.generate_key_pair() - other3_sk, other3_vk = crypto.generate_key_pair() - - first_input_tx = b.get_owned_ids(user_vk).pop() - first_tx = b.create_transaction(user_vk, [other1_vk, other2_vk, other3_vk], first_input_tx, 'TRANSFER') - - first_tx_condition = cc.ThresholdSha256Fulfillment(threshold=2) - first_tx_condition.add_subfulfillment(cc.Ed25519Fulfillment(public_key=other1_vk)) - first_tx_condition.add_subfulfillment(cc.Ed25519Fulfillment(public_key=other2_vk)) - first_tx_condition.add_subfulfillment(cc.Ed25519Fulfillment(public_key=other3_vk)) - - first_tx['transaction']['conditions'][0]['condition'] = { - 'details': first_tx_condition.to_dict(), - 'uri': first_tx_condition.condition.serialize_uri() - } - # conditions have been updated, so hash needs updating - first_tx['id'] = util.get_hash_data(first_tx) - - first_tx_signed = b.sign_transaction(first_tx, user_sk) - - assert b.validate_transaction(first_tx_signed) == first_tx_signed - assert b.is_valid_transaction(first_tx_signed) == first_tx_signed - - b.write_transaction(first_tx_signed) - - # create and write block to bigchain - block = b.create_block([first_tx]) - b.write_block(block, durability='hard') - - next_input_tx = b.get_owned_ids(other1_vk).pop() - # create another transaction with the same input - next_tx = b.create_transaction([other1_vk, other2_vk, other3_vk], user_vk, next_input_tx, 'TRANSFER') - - next_tx_fulfillment = next_tx['transaction']['fulfillments'][0] - next_tx_fulfillment_message = util.get_fulfillment_message(next_tx, next_tx_fulfillment, serialized=True) - - # parse the threshold cryptocondition - next_tx_fulfillment = cc.Fulfillment.from_dict(first_tx['transaction']['conditions'][0]['condition']['details']) - - subfulfillment1 = next_tx_fulfillment.get_subcondition_from_vk(other1_vk)[0] - subfulfillment2 = next_tx_fulfillment.get_subcondition_from_vk(other2_vk)[0] - subfulfillment3 = next_tx_fulfillment.get_subcondition_from_vk(other3_vk)[0] - - next_tx_fulfillment.subconditions = [] - # sign the subconditions until threshold of 2 is reached - subfulfillment1.sign(next_tx_fulfillment_message, crypto.SigningKey(other1_sk)) - next_tx_fulfillment.add_subfulfillment(subfulfillment1) - subfulfillment2.sign(next_tx_fulfillment_message, crypto.SigningKey(other2_sk)) - next_tx_fulfillment.add_subfulfillment(subfulfillment2) - next_tx_fulfillment.add_subcondition(subfulfillment3.condition) - - next_tx['transaction']['fulfillments'][0]['fulfillment'] = next_tx_fulfillment.serialize_uri() - assert b.validate_transaction(next_tx) == next_tx - assert b.is_valid_transaction(next_tx) == next_tx - - @pytest.mark.usefixtures('inputs') - def test_override_condition_and_fulfillment_transfer_threshold_wrongly_signed(self, b, user_vk, user_sk): - other1_sk, other1_vk = crypto.generate_key_pair() - other2_sk, other2_vk = crypto.generate_key_pair() - - first_input_tx = b.get_owned_ids(user_vk).pop() - first_tx = b.create_transaction(user_vk, [other1_vk, other2_vk], first_input_tx, 'TRANSFER') - - first_tx_condition = cc.ThresholdSha256Fulfillment(threshold=2) - first_tx_condition.add_subfulfillment(cc.Ed25519Fulfillment(public_key=other1_vk)) - first_tx_condition.add_subfulfillment(cc.Ed25519Fulfillment(public_key=other2_vk)) - - first_tx['transaction']['conditions'][0]['condition'] = { - 'details': first_tx_condition.to_dict(), - 'uri': first_tx_condition.condition.serialize_uri() - } - # conditions have been updated, so hash needs updating - first_tx['id'] = util.get_hash_data(first_tx) - - first_tx_signed = b.sign_transaction(first_tx, user_sk) - - assert b.validate_transaction(first_tx_signed) == first_tx_signed - assert b.is_valid_transaction(first_tx_signed) == first_tx_signed - - b.write_transaction(first_tx_signed) - - # create and write block to bigchain - block = b.create_block([first_tx]) - b.write_block(block, durability='hard') - - next_input_tx = b.get_owned_ids(other1_vk).pop() - # create another transaction with the same input - next_tx = b.create_transaction([other1_vk, other2_vk], user_vk, next_input_tx, 'TRANSFER') - - next_tx_fulfillment = next_tx['transaction']['fulfillments'][0] - next_tx_fulfillment_message = util.get_fulfillment_message(next_tx, next_tx_fulfillment, serialized=True) - next_tx_fulfillment = cc.ThresholdSha256Fulfillment(threshold=2) - next_tx_subfulfillment1 = cc.Ed25519Fulfillment(public_key=other1_vk) - next_tx_subfulfillment1.sign(next_tx_fulfillment_message, crypto.SigningKey(other1_sk)) - next_tx_fulfillment.add_subfulfillment(next_tx_subfulfillment1) - - # Wrong signing happens here - next_tx_subfulfillment2 = cc.Ed25519Fulfillment(public_key=other1_vk) - next_tx_subfulfillment2.sign(next_tx_fulfillment_message, crypto.SigningKey(other1_sk)) - next_tx_fulfillment.add_subfulfillment(next_tx_subfulfillment2) - next_tx['transaction']['fulfillments'][0]['fulfillment'] = next_tx_fulfillment.serialize_uri() - - with pytest.raises(exceptions.InvalidSignature): - b.validate_transaction(next_tx) - assert b.is_valid_transaction(next_tx) == False - - def test_default_threshold_conditions_for_multiple_owners(self, b, user_sk, user_vk): - user2_sk, user2_vk = crypto.generate_key_pair() - - # create transaction with multiple owners_after - tx = b.create_transaction(b.me, [user_vk, user2_vk], None, 'CREATE') - - assert len(tx['transaction']['conditions']) == 1 - assert len(tx['transaction']['conditions'][0]['condition']['details']['subfulfillments']) == 2 - - # expected condition subfulfillments - expected_condition = cc.ThresholdSha256Fulfillment(threshold=2) - expected_condition.add_subfulfillment(cc.Ed25519Fulfillment(public_key=user_vk)) - expected_condition.add_subfulfillment(cc.Ed25519Fulfillment(public_key=user2_vk)) - tx_expected_condition = { - 'details': expected_condition.to_dict(), - 'uri': expected_condition.condition.serialize_uri() - } - - assert tx['transaction']['conditions'][0]['condition'] == tx_expected_condition - - def test_default_threshold_fulfillments_for_multiple_owners(self, b, user_sk, user_vk): - user2_sk, user2_vk = crypto.generate_key_pair() - - # create transaction with multiple owners_after - tx_create = b.create_transaction(b.me, [user_vk, user2_vk], None, 'CREATE') - tx_create_signed = b.sign_transaction(tx_create, b.me_private) - block = b.create_block([tx_create_signed]) - b.write_block(block, durability='hard') - - inputs = b.get_owned_ids(user_vk) - - # create a transaction with multiple current owners - tx_transfer = b.create_transaction([user_vk, user2_vk], b.me, inputs, 'TRANSFER') - tx_transfer_signed = b.sign_transaction(tx_transfer, [user_sk, user2_sk]) - - # expected fulfillment - expected_fulfillment = cc.Fulfillment.from_dict( - tx_create['transaction']['conditions'][0]['condition']['details']) - subfulfillment1 = expected_fulfillment.subconditions[0]['body'] - subfulfillment2 = expected_fulfillment.subconditions[1]['body'] - - expected_fulfillment_message = util.get_fulfillment_message(tx_transfer, - tx_transfer['transaction']['fulfillments'][0]) - - subfulfillment1.sign(util.serialize(expected_fulfillment_message), crypto.SigningKey(user_sk)) - subfulfillment2.sign(util.serialize(expected_fulfillment_message), crypto.SigningKey(user2_sk)) - - assert tx_transfer_signed['transaction']['fulfillments'][0]['fulfillment'] \ - == expected_fulfillment.serialize_uri() - - assert b.validate_fulfillments(tx_transfer_signed) is True - - def test_create_asset_with_hashlock_condition(self, b): - hashlock_tx = b.create_transaction(b.me, None, None, 'CREATE') - - secret = b'much secret! wow!' - first_tx_condition = cc.PreimageSha256Fulfillment(preimage=secret) - - hashlock_tx['transaction']['conditions'].append({ - 'condition': { - 'details': first_tx_condition.to_dict(), - 'uri': first_tx_condition.condition.serialize_uri() - }, - 'cid': 0, - 'owners_after': None - }) - # conditions have been updated, so hash needs updating - hashlock_tx['id'] = util.get_hash_data(hashlock_tx) - - hashlock_tx_signed = b.sign_transaction(hashlock_tx, b.me_private) - - assert b.validate_transaction(hashlock_tx_signed) == hashlock_tx_signed - assert b.is_valid_transaction(hashlock_tx_signed) == hashlock_tx_signed - - b.write_transaction(hashlock_tx_signed) - - # create and write block to bigchain - block = b.create_block([hashlock_tx_signed]) - b.write_block(block, durability='hard') - - @pytest.mark.usefixtures('inputs') - def test_transfer_asset_with_hashlock_condition(self, b, user_vk, user_sk): - owned_count = len(b.get_owned_ids(user_vk)) - first_input_tx = b.get_owned_ids(user_vk).pop() - - hashlock_tx = b.create_transaction(user_vk, None, first_input_tx, 'TRANSFER') - - secret = b'much secret! wow!' - first_tx_condition = cc.PreimageSha256Fulfillment(preimage=secret) - - hashlock_tx['transaction']['conditions'].append({ - 'condition': { - 'details': first_tx_condition.to_dict(), - 'uri': first_tx_condition.condition.serialize_uri() - }, - 'cid': 0, - 'owners_after': None - }) - # conditions have been updated, so hash needs updating - hashlock_tx['id'] = util.get_hash_data(hashlock_tx) - - hashlock_tx_signed = b.sign_transaction(hashlock_tx, user_sk) - - assert b.validate_transaction(hashlock_tx_signed) == hashlock_tx_signed - assert b.is_valid_transaction(hashlock_tx_signed) == hashlock_tx_signed - assert len(b.get_owned_ids(user_vk)) == owned_count - - b.write_transaction(hashlock_tx_signed) - - # create and write block to bigchain - block = b.create_block([hashlock_tx_signed]) - b.write_block(block, durability='hard') - - assert len(b.get_owned_ids(user_vk)) == owned_count - 1 - - def test_create_and_fulfill_asset_with_hashlock_condition(self, b, user_vk): - hashlock_tx = b.create_transaction(b.me, None, None, 'CREATE') - - secret = b'much secret! wow!' - first_tx_condition = cc.PreimageSha256Fulfillment(preimage=secret) - - hashlock_tx['transaction']['conditions'].append({ - 'condition': { - 'details': first_tx_condition.to_dict(), - 'uri': first_tx_condition.condition.serialize_uri() - }, - 'cid': 0, - 'owners_after': None - }) - # conditions have been updated, so hash needs updating - hashlock_tx['id'] = util.get_hash_data(hashlock_tx) - - hashlock_tx_signed = b.sign_transaction(hashlock_tx, b.me_private) - - assert b.validate_transaction(hashlock_tx_signed) == hashlock_tx_signed - assert b.is_valid_transaction(hashlock_tx_signed) == hashlock_tx_signed - - b.write_transaction(hashlock_tx_signed) - - # create and write block to bigchain - block = b.create_block([hashlock_tx_signed]) - b.write_block(block, durability='hard') - - assert len(b.get_owned_ids(b.me)) == 0 - - # create hashlock fulfillment tx - hashlock_fulfill_tx = b.create_transaction(None, user_vk, {'txid': hashlock_tx['id'], 'cid': 0}, 'TRANSFER') - - hashlock_fulfill_tx_fulfillment = cc.PreimageSha256Fulfillment(preimage=b'') - hashlock_fulfill_tx['transaction']['fulfillments'][0]['fulfillment'] = \ - hashlock_fulfill_tx_fulfillment.serialize_uri() - - with pytest.raises(exceptions.InvalidSignature): - b.validate_transaction(hashlock_fulfill_tx) - assert b.is_valid_transaction(hashlock_fulfill_tx) == False - - hashlock_fulfill_tx_fulfillment = cc.PreimageSha256Fulfillment(preimage=secret) - hashlock_fulfill_tx['transaction']['fulfillments'][0]['fulfillment'] = \ - hashlock_fulfill_tx_fulfillment.serialize_uri() - - assert b.validate_transaction(hashlock_fulfill_tx) == hashlock_fulfill_tx - assert b.is_valid_transaction(hashlock_fulfill_tx) == hashlock_fulfill_tx - - b.write_transaction(hashlock_fulfill_tx) - - # create and write block to bigchain - block = b.create_block([hashlock_fulfill_tx]) - b.write_block(block, durability='hard') - - assert len(b.get_owned_ids(b.me)) == 0 - assert len(b.get_owned_ids(user_vk)) == 1 - - # try doublespending - user2_sk, user2_vk = crypto.generate_key_pair() - hashlock_doublespend_tx = b.create_transaction(None, user2_vk, {'txid': hashlock_tx['id'], 'cid': 0}, - 'TRANSFER') - - hashlock_doublespend_tx_fulfillment = cc.PreimageSha256Fulfillment(preimage=secret) - hashlock_doublespend_tx['transaction']['fulfillments'][0]['fulfillment'] = \ - hashlock_doublespend_tx_fulfillment.serialize_uri() - - with pytest.raises(exceptions.DoubleSpend): - b.validate_transaction(hashlock_doublespend_tx) - - def test_get_subcondition_from_vk(self, b, user_sk, user_vk): - user2_sk, user2_vk = crypto.generate_key_pair() - user3_sk, user3_vk = crypto.generate_key_pair() - user4_sk, user4_vk = crypto.generate_key_pair() - user5_sk, user5_vk = crypto.generate_key_pair() - owners_after = [user_vk, user2_vk, user3_vk, user4_vk, user5_vk] - - # create a transaction with multiple owners_after - tx = b.create_transaction(b.me, owners_after, None, 'CREATE') - condition = cc.Fulfillment.from_dict(tx['transaction']['conditions'][0]['condition']['details']) - - for owner_after in owners_after: - subcondition = condition.get_subcondition_from_vk(owner_after)[0] - assert subcondition.public_key.to_ascii().decode() == owner_after - - @pytest.mark.usefixtures('inputs') - def test_transfer_asset_with_escrow_condition(self, b, user_vk, user_sk): - first_input_tx = b.get_owned_ids(user_vk).pop() - user2_sk, user2_vk = crypto.generate_key_pair() - - # ESCROW - escrow_tx = b.create_transaction(user_vk, [user_vk, user2_vk], first_input_tx, 'TRANSFER') - - time_sleep = 3 - - condition_escrow = cc.ThresholdSha256Fulfillment(threshold=1) - fulfillment_timeout = cc.TimeoutFulfillment(expire_time=str(float(util.timestamp()) + time_sleep)) - fulfillment_timeout_inverted = cc.InvertedThresholdSha256Fulfillment(threshold=1) - fulfillment_timeout_inverted.add_subfulfillment(fulfillment_timeout) # invert the timeout condition - condition_user = cc.Ed25519Fulfillment(public_key=user_vk) - condition_user2 = cc.Ed25519Fulfillment(public_key=user2_vk) - - # execute branch - fulfillment_and_execute = cc.ThresholdSha256Fulfillment(threshold=2) - fulfillment_and_execute.add_subfulfillment(condition_user2) - fulfillment_and_execute.add_subfulfillment(fulfillment_timeout) - - # do not fulfill abort branch - fulfillment_and_abort = cc.ThresholdSha256Fulfillment(threshold=2) - fulfillment_and_abort.add_subfulfillment(condition_user) - fulfillment_and_abort.add_subfulfillment(fulfillment_timeout_inverted) - - condition_escrow.add_subfulfillment(fulfillment_and_execute) - condition_escrow.add_subfulfillment(fulfillment_and_abort) - - # Update the condition in the newly created transaction - escrow_tx['transaction']['conditions'][0]['condition'] = { - 'details': condition_escrow.to_dict(), - 'uri': condition_escrow.condition.serialize_uri() - } - - # conditions have been updated, so hash needs updating - escrow_tx['id'] = util.get_hash_data(escrow_tx) - - escrow_tx_signed = b.sign_transaction(escrow_tx, user_sk) - - assert b.validate_transaction(escrow_tx_signed) == escrow_tx_signed - assert b.is_valid_transaction(escrow_tx_signed) == escrow_tx_signed - - b.write_transaction(escrow_tx_signed) - - # create and write block to bigchain - block = b.create_block([escrow_tx_signed]) - b.write_block(block, durability='hard') - - # Retrieve the last transaction of thresholduser1_pub - tx_retrieved_id = b.get_owned_ids(user2_vk).pop() - - # EXECUTE - # Create a base template for output transaction - escrow_tx_transfer = b.create_transaction([user_vk, user2_vk], user2_vk, tx_retrieved_id, 'TRANSFER') - - # Parse the threshold cryptocondition - escrow_fulfillment = cc.Fulfillment.from_dict( - escrow_tx['transaction']['conditions'][0]['condition']['details']) - - subfulfillment_user = escrow_fulfillment.get_subcondition_from_vk(user_vk)[0] - subfulfillment_user2 = escrow_fulfillment.get_subcondition_from_vk(user2_vk)[0] - - # Get the fulfillment message to sign - escrow_tx_fulfillment_message = util.get_fulfillment_message(escrow_tx_transfer, - escrow_tx_transfer['transaction']['fulfillments'][0], - serialized=True) - escrow_fulfillment.subconditions = [] - # fulfill execute branch - fulfillment_and_execute = cc.ThresholdSha256Fulfillment(threshold=2) - subfulfillment_user2.sign(escrow_tx_fulfillment_message, crypto.SigningKey(user2_sk)) - fulfillment_and_execute.add_subfulfillment(subfulfillment_user2) - fulfillment_and_execute.add_subfulfillment(fulfillment_timeout) - escrow_fulfillment.add_subfulfillment(fulfillment_and_execute) - - # do not fulfill abort branch - fulfillment_and_abort = cc.ThresholdSha256Fulfillment(threshold=2) - fulfillment_and_abort.add_subfulfillment(subfulfillment_user) - fulfillment_and_abort.add_subfulfillment(fulfillment_timeout_inverted) - escrow_fulfillment.add_subcondition(fulfillment_and_abort.condition) - - escrow_tx_transfer['transaction']['fulfillments'][0]['fulfillment'] = escrow_fulfillment.serialize_uri() - - # in-time validation (execute) - assert b.is_valid_transaction(escrow_tx_transfer) == escrow_tx_transfer - assert b.validate_transaction(escrow_tx_transfer) == escrow_tx_transfer - - time.sleep(time_sleep + 1) - - assert b.is_valid_transaction(escrow_tx_transfer) is False - with pytest.raises(exceptions.InvalidSignature): - assert b.validate_transaction(escrow_tx_transfer) == escrow_tx_transfer - - # ABORT - # Create a base template for output transaction - escrow_tx_abort = b.create_transaction([user_vk, user2_vk], user_vk, tx_retrieved_id, 'TRANSFER') - - # Parse the threshold cryptocondition - escrow_fulfillment = cc.Fulfillment.from_dict( - escrow_tx['transaction']['conditions'][0]['condition']['details']) - - subfulfillment_user = escrow_fulfillment.get_subcondition_from_vk(user_vk)[0] - subfulfillment_user2 = escrow_fulfillment.get_subcondition_from_vk(user2_vk)[0] - - # Get the fulfillment message to sign - escrow_tx_fulfillment_message = util.get_fulfillment_message(escrow_tx_abort, - escrow_tx_abort['transaction']['fulfillments'][0], - serialized=True) - escrow_fulfillment.subconditions = [] - # fulfill execute branch - fulfillment_and_execute = cc.ThresholdSha256Fulfillment(threshold=2) - fulfillment_and_execute.add_subfulfillment(subfulfillment_user2) - fulfillment_and_execute.add_subfulfillment(fulfillment_timeout) - escrow_fulfillment.add_subcondition(fulfillment_and_execute.condition) - - # do not fulfill abort branch - fulfillment_and_abort = cc.ThresholdSha256Fulfillment(threshold=2) - subfulfillment_user.sign(escrow_tx_fulfillment_message, crypto.SigningKey(user_sk)) - fulfillment_and_abort.add_subfulfillment(subfulfillment_user) - fulfillment_and_abort.add_subfulfillment(fulfillment_timeout_inverted) - escrow_fulfillment.add_subfulfillment(fulfillment_and_abort) - - escrow_tx_abort['transaction']['fulfillments'][0]['fulfillment'] = escrow_fulfillment.serialize_uri() - - # out-of-time validation (abort) - assert b.validate_transaction(escrow_tx_abort) == escrow_tx_abort - assert b.is_valid_transaction(escrow_tx_abort) == escrow_tx_abort - - @pytest.mark.usefixtures('inputs') - def test_transfer_asset_with_escrow_condition_doublespend(self, b, user_vk, user_sk): - first_input_tx = b.get_owned_ids(user_vk).pop() - user2_sk, user2_vk = crypto.generate_key_pair() - - # ESCROW - escrow_tx = b.create_transaction(user_vk, [user_vk, user2_vk], first_input_tx, 'TRANSFER') - - time_sleep = 3 - - condition_escrow = cc.ThresholdSha256Fulfillment(threshold=1) - fulfillment_timeout = cc.TimeoutFulfillment(expire_time=str(float(util.timestamp()) + time_sleep)) - fulfillment_timeout_inverted = cc.InvertedThresholdSha256Fulfillment(threshold=1) - fulfillment_timeout_inverted.add_subfulfillment(fulfillment_timeout) # invert the timeout condition - condition_user = cc.Ed25519Fulfillment(public_key=user_vk) - condition_user2 = cc.Ed25519Fulfillment(public_key=user2_vk) - - # execute branch - fulfillment_and_execute = cc.ThresholdSha256Fulfillment(threshold=2) - fulfillment_and_execute.add_subfulfillment(condition_user2) - fulfillment_and_execute.add_subfulfillment(fulfillment_timeout) - - # do not fulfill abort branch - fulfillment_and_abort = cc.ThresholdSha256Fulfillment(threshold=2) - fulfillment_and_abort.add_subfulfillment(condition_user) - fulfillment_and_abort.add_subfulfillment(fulfillment_timeout_inverted) - - condition_escrow.add_subfulfillment(fulfillment_and_execute) - condition_escrow.add_subfulfillment(fulfillment_and_abort) - - # Update the condition in the newly created transaction - escrow_tx['transaction']['conditions'][0]['condition'] = { - 'details': condition_escrow.to_dict(), - 'uri': condition_escrow.condition.serialize_uri() - } - - # conditions have been updated, so hash needs updating - escrow_tx['id'] = util.get_hash_data(escrow_tx) - - escrow_tx_signed = b.sign_transaction(escrow_tx, user_sk) - - assert b.validate_transaction(escrow_tx_signed) == escrow_tx_signed - assert b.is_valid_transaction(escrow_tx_signed) == escrow_tx_signed - - b.write_transaction(escrow_tx_signed) - - # create and write block to bigchain - block = b.create_block([escrow_tx_signed]) - b.write_block(block, durability='hard') - - # Retrieve the last transaction of thresholduser1_pub - tx_retrieved_id = b.get_owned_ids(user2_vk).pop() - - # EXECUTE - # Create a base template for output transaction - escrow_tx_transfer = b.create_transaction([user_vk, user2_vk], user2_vk, tx_retrieved_id, 'TRANSFER') - - # Parse the threshold cryptocondition - escrow_fulfillment = cc.Fulfillment.from_dict( - escrow_tx['transaction']['conditions'][0]['condition']['details']) - - subfulfillment_user = escrow_fulfillment.get_subcondition_from_vk(user_vk)[0] - subfulfillment_user2 = escrow_fulfillment.get_subcondition_from_vk(user2_vk)[0] - - # Get the fulfillment message to sign - escrow_tx_fulfillment_message = util.get_fulfillment_message(escrow_tx_transfer, - escrow_tx_transfer['transaction']['fulfillments'][0], - serialized=True) - escrow_fulfillment.subconditions = [] - # fulfill execute branch - fulfillment_and_execute = cc.ThresholdSha256Fulfillment(threshold=2) - subfulfillment_user2.sign(escrow_tx_fulfillment_message, crypto.SigningKey(user2_sk)) - fulfillment_and_execute.add_subfulfillment(subfulfillment_user2) - fulfillment_and_execute.add_subfulfillment(fulfillment_timeout) - escrow_fulfillment.add_subfulfillment(fulfillment_and_execute) - - # do not fulfill abort branch - fulfillment_and_abort = cc.ThresholdSha256Fulfillment(threshold=2) - fulfillment_and_abort.add_subfulfillment(subfulfillment_user) - fulfillment_and_abort.add_subfulfillment(fulfillment_timeout_inverted) - escrow_fulfillment.add_subcondition(fulfillment_and_abort.condition) - - escrow_tx_transfer['transaction']['fulfillments'][0]['fulfillment'] = escrow_fulfillment.serialize_uri() - - # in-time validation (execute) - assert b.is_valid_transaction(escrow_tx_transfer) == escrow_tx_transfer - assert b.validate_transaction(escrow_tx_transfer) == escrow_tx_transfer - - b.write_transaction(escrow_tx_transfer) - - # create and write block to bigchain - block = b.create_block([escrow_tx_transfer]) - b.write_block(block, durability='hard') - - time.sleep(time_sleep + 1) - - assert b.is_valid_transaction(escrow_tx_transfer) is False - with pytest.raises(exceptions.InvalidSignature): - assert b.validate_transaction(escrow_tx_transfer) == escrow_tx_transfer - - # ABORT - # Create a base template for output transaction - escrow_tx_abort = b.create_transaction([user_vk, user2_vk], user_vk, tx_retrieved_id, 'TRANSFER') - - # Parse the threshold cryptocondition - escrow_fulfillment = cc.Fulfillment.from_dict( - escrow_tx['transaction']['conditions'][0]['condition']['details']) - - subfulfillment_user = escrow_fulfillment.get_subcondition_from_vk(user_vk)[0] - subfulfillment_user2 = escrow_fulfillment.get_subcondition_from_vk(user2_vk)[0] - - # Get the fulfillment message to sign - escrow_tx_fulfillment_message = util.get_fulfillment_message(escrow_tx_abort, - escrow_tx_abort['transaction']['fulfillments'][0], - serialized=True) - escrow_fulfillment.subconditions = [] - # do not fulfill execute branch - fulfillment_and_execute = cc.ThresholdSha256Fulfillment(threshold=2) - fulfillment_and_execute.add_subfulfillment(subfulfillment_user2) - fulfillment_and_execute.add_subfulfillment(fulfillment_timeout) - escrow_fulfillment.add_subcondition(fulfillment_and_execute.condition) - - # fulfill abort branch - fulfillment_and_abort = cc.ThresholdSha256Fulfillment(threshold=2) - subfulfillment_user.sign(escrow_tx_fulfillment_message, crypto.SigningKey(user_sk)) - fulfillment_and_abort.add_subfulfillment(subfulfillment_user) - fulfillment_and_abort.add_subfulfillment(fulfillment_timeout_inverted) - escrow_fulfillment.add_subfulfillment(fulfillment_and_abort) - - escrow_tx_abort['transaction']['fulfillments'][0]['fulfillment'] = escrow_fulfillment.serialize_uri() - - # out-of-time validation (abort) - with pytest.raises(exceptions.DoubleSpend): - b.validate_transaction(escrow_tx_abort) - assert b.is_valid_transaction(escrow_tx_abort) is False - + for unspent in transactions[1:]: + assert b.get_spent(unspent.id, 0) is None diff --git a/tests/db/test_utils.py b/tests/db/test_utils.py index 957373f9..f22bcd04 100644 --- a/tests/db/test_utils.py +++ b/tests/db/test_utils.py @@ -1,5 +1,6 @@ import builtins +from bigchaindb_common import exceptions import pytest import rethinkdb as r @@ -150,7 +151,7 @@ def test_init_fails_if_db_exists(): # The db is set up by fixtures assert r.db_list().contains(dbname).run(conn) is True - with pytest.raises(bigchaindb.exceptions.DatabaseAlreadyExists): + with pytest.raises(exceptions.DatabaseAlreadyExists): utils.init() @@ -200,6 +201,6 @@ def test_drop_non_existent_db_raises_an_error(): assert r.db_list().contains(dbname).run(conn) is True utils.drop(assume_yes=True) - with pytest.raises(bigchaindb.exceptions.DatabaseDoesNotExist): + with pytest.raises(exceptions.DatabaseDoesNotExist): utils.drop(assume_yes=True) diff --git a/tests/doc/run_doc_python_server_api_examples.py b/tests/doc/run_doc_python_server_api_examples.py index 63bd899e..b1f0ed9b 100644 --- a/tests/doc/run_doc_python_server_api_examples.py +++ b/tests/doc/run_doc_python_server_api_examples.py @@ -2,6 +2,7 @@ import json from time import sleep import cryptoconditions as cc +from bigchaindb_common.util import gen_timestamp from bigchaindb import Bigchain, util, crypto, exceptions @@ -315,7 +316,7 @@ tx_timeout = b.create_transaction(b.me, None, None, 'CREATE') # Set expiry time (12 secs from now) time_sleep = 12 -time_expire = str(float(util.timestamp()) + time_sleep) +time_expire = str(float(gen_timestamp()) + time_sleep) # only valid if the server time <= time_expire condition_timeout = cc.TimeoutFulfillment(expire_time=time_expire) @@ -355,7 +356,7 @@ tx_timeout_transfer['transaction']['fulfillments'][0]['fulfillment'] = timeout_f # no need to sign transaction, like with hashlocks for i in range(time_sleep - 4): tx_timeout_valid = b.is_valid_transaction(tx_timeout_transfer) == tx_timeout_transfer - seconds_to_timeout = int(float(time_expire) - float(util.timestamp())) + seconds_to_timeout = int(float(time_expire) - float(gen_timestamp())) print('tx_timeout valid: {} ({}s to timeout)'.format(tx_timeout_valid, seconds_to_timeout)) sleep(1) @@ -370,7 +371,7 @@ tx_escrow = b.create_transaction(testuser2_pub, [testuser2_pub, testuser1_pub], # Set expiry time (12 secs from now) time_sleep = 12 -time_expire = str(float(util.timestamp()) + time_sleep) +time_expire = str(float(gen_timestamp()) + time_sleep) # Create escrow and timeout condition condition_escrow = cc.ThresholdSha256Fulfillment(threshold=1) # OR Gate @@ -486,6 +487,6 @@ for i in range(time_sleep - 4): valid_execute = b.is_valid_transaction(tx_escrow_execute) == tx_escrow_execute valid_abort = b.is_valid_transaction(tx_escrow_abort) == tx_escrow_abort - seconds_to_timeout = int(float(time_expire) - float(util.timestamp())) + seconds_to_timeout = int(float(time_expire) - float(gen_timestamp())) print('tx_execute valid: {} - tx_abort valid {} ({}s to timeout)'.format(valid_execute, valid_abort, seconds_to_timeout)) sleep(1) diff --git a/tests/pipelines/conftest.py b/tests/pipelines/conftest.py index 1a3a77e2..a949c947 100644 --- a/tests/pipelines/conftest.py +++ b/tests/pipelines/conftest.py @@ -16,4 +16,3 @@ def setup_database(request, node_config): @pytest.fixture(scope='function', autouse=True) def cleanup_tables(request, node_config): conftest.cleanup_tables(request, node_config) - diff --git a/tests/pipelines/test_block_creation.py b/tests/pipelines/test_block_creation.py index a1ab6a19..8380dd42 100644 --- a/tests/pipelines/test_block_creation.py +++ b/tests/pipelines/test_block_creation.py @@ -1,20 +1,18 @@ import time -import random from unittest.mock import patch import rethinkdb as r -from bigchaindb.pipelines import block -from multipipes import Pipe, Pipeline +from multipipes import Pipe -def test_filter_by_assignee(b, user_vk): - block_maker = block.Block() +def test_filter_by_assignee(b, signed_create_tx): + from bigchaindb.pipelines.block import BlockPipeline - tx = b.create_transaction(b.me, user_vk, None, 'CREATE') - tx = b.sign_transaction(tx, b.me_private) - tx['assignee'] = b.me - tx['assignment_timestamp'] = 111 + block_maker = BlockPipeline() + + tx = signed_create_tx.to_dict() + tx.update({'assignee': b.me, 'assignment_timestamp': 111}) # filter_tx has side effects on the `tx` instance by popping 'assignee' # and 'assignment_timestamp' @@ -23,91 +21,97 @@ def test_filter_by_assignee(b, user_vk): assert 'assignee' not in filtered_tx assert 'assignment_timestamp' not in filtered_tx - tx = b.create_transaction(b.me, user_vk, None, 'CREATE') - tx = b.sign_transaction(tx, b.me_private) - tx['assignee'] = 'nobody' + tx = signed_create_tx.to_dict() + tx.update({'assignee': 'nobody'}) assert block_maker.filter_tx(tx) is None -def test_validate_transaction(b, user_vk): - block_maker = block.Block() +def test_validate_transaction(b, create_tx): + from bigchaindb.pipelines.block import BlockPipeline - tx = b.create_transaction(b.me, user_vk, None, 'CREATE') - tx = b.sign_transaction(tx, b.me_private) - tx['id'] = 'a' * 64 + block_maker = BlockPipeline() - assert block_maker.validate_tx(tx) is None + assert block_maker.validate_tx(create_tx.to_dict()) is None - tx = b.create_transaction(b.me, user_vk, None, 'CREATE') - tx = b.sign_transaction(tx, b.me_private) - - assert block_maker.validate_tx(tx) == tx + valid_tx = create_tx.sign([b.me_private]) + assert block_maker.validate_tx(valid_tx.to_dict()) == valid_tx def test_create_block(b, user_vk): - block_maker = block.Block() + from bigchaindb.models import Transaction + from bigchaindb.pipelines.block import BlockPipeline + + block_maker = BlockPipeline() for i in range(100): - tx = b.create_transaction(b.me, user_vk, None, 'CREATE') - tx = b.sign_transaction(tx, b.me_private) + tx = Transaction.create([b.me], [user_vk]) + tx = tx.sign([b.me_private]) block_maker.create(tx) # force the output triggering a `timeout` block_doc = block_maker.create(None, timeout=True) - assert len(block_doc['block']['transactions']) == 100 + assert len(block_doc.transactions) == 100 def test_write_block(b, user_vk): - block_maker = block.Block() + from bigchaindb.models import Block, Transaction + from bigchaindb.pipelines.block import BlockPipeline + + block_maker = BlockPipeline() txs = [] for i in range(100): - tx = b.create_transaction(b.me, user_vk, None, 'CREATE') - tx = b.sign_transaction(tx, b.me_private) + tx = Transaction.create([b.me], [user_vk]) + tx = tx.sign([b.me_private]) txs.append(tx) block_doc = b.create_block(txs) block_maker.write(block_doc) + expected = r.table('bigchain').get(block_doc.id).run(b.conn) + expected = Block.from_dict(expected) - assert r.table('bigchain').get(block_doc['id']).run(b.conn) == block_doc + assert expected == block_doc def test_duplicate_transaction(b, user_vk): - block_maker = block.Block() + from bigchaindb.models import Transaction + from bigchaindb.pipelines import block + block_maker = block.BlockPipeline() txs = [] for i in range(10): - tx = b.create_transaction(b.me, user_vk, None, 'CREATE') - tx = b.sign_transaction(tx, b.me_private) + tx = Transaction.create([b.me], [user_vk]) + tx = tx.sign([b.me_private]) txs.append(tx) block_doc = b.create_block(txs) block_maker.write(block_doc) # block is in bigchain - assert r.table('bigchain').get(block_doc['id']).run(b.conn) == block_doc + assert r.table('bigchain').get(block_doc.id).run(b.conn) == block_doc.to_dict() b.write_transaction(txs[0]) # verify tx is in the backlog - assert r.table('backlog').get(txs[0]['id']).run(b.conn) is not None + assert r.table('backlog').get(txs[0].id).run(b.conn) is not None # try to validate a transaction that's already in the chain; should not # work - assert block_maker.validate_tx(txs[0]) is None + assert block_maker.validate_tx(txs[0].to_dict()) is None # duplicate tx should be removed from backlog - assert r.table('backlog').get(txs[0]['id']).run(b.conn) is None + assert r.table('backlog').get(txs[0].id).run(b.conn) is None def test_delete_tx(b, user_vk): - block_maker = block.Block() - + from bigchaindb.models import Transaction + from bigchaindb.pipelines.block import BlockPipeline + block_maker = BlockPipeline() for i in range(100): - tx = b.create_transaction(b.me, user_vk, None, 'CREATE') - tx = b.sign_transaction(tx, b.me_private) + tx = Transaction.create([b.me], [user_vk]) + tx = tx.sign([b.me_private]) block_maker.create(tx) # make sure the tx appears in the backlog b.write_transaction(tx) @@ -115,7 +119,7 @@ def test_delete_tx(b, user_vk): # force the output triggering a `timeout` block_doc = block_maker.create(None, timeout=True) - for tx in block_doc['block']['transactions']: + for tx in block_doc.to_dict()['block']['transactions']: returned_tx = r.table('backlog').get(tx['id']).run(b.conn) returned_tx.pop('assignee') returned_tx.pop('assignment_timestamp') @@ -125,37 +129,47 @@ def test_delete_tx(b, user_vk): assert returned_block == block_doc - for tx in block_doc['block']['transactions']: + for tx in block_doc.to_dict()['block']['transactions']: assert r.table('backlog').get(tx['id']).run(b.conn) is None def test_prefeed(b, user_vk): + import random + from bigchaindb.models import Transaction + from bigchaindb.pipelines.block import initial + for i in range(100): - tx = b.create_transaction(b.me, user_vk, None, 'CREATE') - tx = b.sign_transaction(tx, b.me_private) + tx = Transaction.create([b.me], [user_vk], {'msg': random.random()}) + tx = tx.sign([b.me_private]) b.write_transaction(tx) - backlog = block.initial() + backlog = initial() assert len(list(backlog)) == 100 -@patch.object(Pipeline, 'start') -def test_start(mock_start): - # TODO: `block.start` is just a wrapper around `block.create_pipeline`, - # that is tested by `test_full_pipeline`. - # If anyone has better ideas on how to test this, please do a PR :) - block.start() - mock_start.assert_called_with() +@patch('bigchaindb.pipelines.block.create_pipeline') +def test_start(create_pipeline): + from bigchaindb.pipelines import block + + pipeline = block.start() + assert create_pipeline.called + assert create_pipeline.return_value.setup.called + assert create_pipeline.return_value.start.called + assert pipeline == create_pipeline.return_value def test_full_pipeline(b, user_vk): + import random + from bigchaindb.models import Block, Transaction + from bigchaindb.pipelines.block import create_pipeline, get_changefeed + outpipe = Pipe() count_assigned_to_me = 0 for i in range(100): - tx = b.create_transaction(b.me, user_vk, None, 'CREATE') - tx = b.sign_transaction(tx, b.me_private) + tx = Transaction.create([b.me], [user_vk], {'msg': random.random()}) + tx = tx.sign([b.me_private]).to_dict() assignee = random.choice([b.me, 'aaa', 'bbb', 'ccc']) tx['assignee'] = assignee tx['assignment_timestamp'] = time.time() @@ -165,16 +179,17 @@ def test_full_pipeline(b, user_vk): assert r.table('backlog').count().run(b.conn) == 100 - pipeline = block.create_pipeline() - pipeline.setup(indata=block.get_changefeed(), outdata=outpipe) + pipeline = create_pipeline() + pipeline.setup(indata=get_changefeed(), outdata=outpipe) pipeline.start() time.sleep(2) pipeline.terminate() block_doc = outpipe.get() + chained_block = r.table('bigchain').get(block_doc.id).run(b.conn) + chained_block = Block.from_dict(chained_block) - assert len(block_doc['block']['transactions']) == count_assigned_to_me - assert r.table('bigchain').get(block_doc['id']).run(b.conn) == block_doc + assert len(block_doc.transactions) == count_assigned_to_me + assert chained_block == block_doc assert r.table('backlog').count().run(b.conn) == 100 - count_assigned_to_me - diff --git a/tests/pipelines/test_election.py b/tests/pipelines/test_election.py index 02a0e39d..62ff3fec 100644 --- a/tests/pipelines/test_election.py +++ b/tests/pipelines/test_election.py @@ -1,19 +1,21 @@ import time -import random -from bigchaindb import crypto, Bigchain from unittest.mock import patch +from bigchaindb_common import crypto import rethinkdb as r - -from bigchaindb.pipelines import election from multipipes import Pipe, Pipeline +from bigchaindb import Bigchain +from bigchaindb.pipelines import election + def test_check_for_quorum_invalid(b, user_vk): + from bigchaindb.models import Transaction + e = election.Election() # create blocks with transactions - tx1 = b.create_transaction(b.me, user_vk, None, 'CREATE') + tx1 = Transaction.create([b.me], [user_vk]) test_block = b.create_block([tx1]) # simulate a federation with four voters @@ -22,12 +24,13 @@ def test_check_for_quorum_invalid(b, user_vk): for key_pair in key_pairs] # add voters to block and write - test_block['block']['voters'] = [key_pair[1] for key_pair in key_pairs] + test_block.voters = [key_pair[1] for key_pair in key_pairs] + test_block = test_block.sign(b.me_private) b.write_block(test_block) # split_vote (invalid) - votes = [member.vote(test_block['id'], 'abc', True) for member in test_federation[:2]] + \ - [member.vote(test_block['id'], 'abc', False) for member in test_federation[2:]] + votes = [member.vote(test_block.id, 'abc', True) for member in test_federation[:2]] + \ + [member.vote(test_block.id, 'abc', False) for member in test_federation[2:]] # cast votes r.table('votes').insert(votes, durability='hard').run(b.conn) @@ -37,10 +40,11 @@ def test_check_for_quorum_invalid(b, user_vk): def test_check_for_quorum_invalid_prev_node(b, user_vk): + from bigchaindb.models import Transaction e = election.Election() # create blocks with transactions - tx1 = b.create_transaction(b.me, user_vk, None, 'CREATE') + tx1 = Transaction.create([b.me], [user_vk]) test_block = b.create_block([tx1]) # simulate a federation with four voters @@ -49,12 +53,13 @@ def test_check_for_quorum_invalid_prev_node(b, user_vk): for key_pair in key_pairs] # add voters to block and write - test_block['block']['voters'] = [key_pair[1] for key_pair in key_pairs] + test_block.voters = [key_pair[1] for key_pair in key_pairs] + test_block = test_block.sign(b.me_private) b.write_block(test_block) # split vote over prev node - votes = [member.vote(test_block['id'], 'abc', True) for member in test_federation[:2]] + \ - [member.vote(test_block['id'], 'def', True) for member in test_federation[2:]] + votes = [member.vote(test_block.id, 'abc', True) for member in test_federation[:2]] + \ + [member.vote(test_block.id, 'def', True) for member in test_federation[2:]] # cast votes r.table('votes').insert(votes, durability='hard').run(b.conn) @@ -64,10 +69,12 @@ def test_check_for_quorum_invalid_prev_node(b, user_vk): def test_check_for_quorum_valid(b, user_vk): + from bigchaindb.models import Transaction + e = election.Election() # create blocks with transactions - tx1 = b.create_transaction(b.me, user_vk, None, 'CREATE') + tx1 = Transaction.create([b.me], [user_vk]) test_block = b.create_block([tx1]) # simulate a federation with four voters @@ -76,12 +83,13 @@ def test_check_for_quorum_valid(b, user_vk): for key_pair in key_pairs] # add voters to block and write - test_block['block']['voters'] = [key_pair[1] for key_pair in key_pairs] + test_block.voters = [key_pair[1] for key_pair in key_pairs] + test_block = test_block.sign(b.me_private) b.write_block(test_block) # votes for block one - votes = [member.vote(test_block['id'], 'abc', True) - for member in test_federation] + votes = [member.vote(test_block.id, 'abc', True) + for member in test_federation] # cast votes r.table('votes').insert(votes, durability='hard').run(b.conn) @@ -90,18 +98,19 @@ def test_check_for_quorum_valid(b, user_vk): def test_check_requeue_transaction(b, user_vk): + from bigchaindb.models import Transaction + e = election.Election() # create blocks with transactions - tx1 = b.create_transaction(b.me, user_vk, None, 'CREATE') + tx1 = Transaction.create([b.me], [user_vk]) test_block = b.create_block([tx1]) e.requeue_transactions(test_block) - tx_backlog = r.table('backlog').get(tx1['id']).run(b.conn) - tx_backlog.pop('assignee') - tx_backlog.pop('assignment_timestamp') - - assert tx_backlog == tx1 + backlog_tx = r.table('backlog').get(tx1.id).run(b.conn) + backlog_tx.pop('assignee') + backlog_tx.pop('assignment_timestamp') + assert backlog_tx == tx1.to_dict() @patch.object(Pipeline, 'start') @@ -114,13 +123,16 @@ def test_start(mock_start): def test_full_pipeline(b, user_vk): + import random + from bigchaindb.models import Transaction + outpipe = Pipe() # write two blocks txs = [] for i in range(100): - tx = b.create_transaction(b.me, user_vk, None, 'CREATE') - tx = b.sign_transaction(tx, b.me_private) + tx = Transaction.create([b.me], [user_vk], {'msg': random.random()}) + tx = tx.sign([b.me_private]) txs.append(tx) valid_block = b.create_block(txs) @@ -128,8 +140,8 @@ def test_full_pipeline(b, user_vk): txs = [] for i in range(100): - tx = b.create_transaction(b.me, user_vk, None, 'CREATE') - tx = b.sign_transaction(tx, b.me_private) + tx = Transaction.create([b.me], [user_vk], {'msg': random.random()}) + tx = tx.sign([b.me_private]) txs.append(tx) invalid_block = b.create_block(txs) @@ -140,8 +152,8 @@ def test_full_pipeline(b, user_vk): pipeline.start() time.sleep(1) # vote one block valid, one invalid - vote_valid = b.vote(valid_block['id'], 'abc', True) - vote_invalid = b.vote(invalid_block['id'], 'abc', False) + vote_valid = b.vote(valid_block.id, 'abc', True) + vote_invalid = b.vote(invalid_block.id, 'abc', False) r.table('votes').insert(vote_valid, durability='hard').run(b.conn) r.table('votes').insert(vote_invalid, durability='hard').run(b.conn) @@ -152,6 +164,7 @@ def test_full_pipeline(b, user_vk): # only transactions from the invalid block should be returned to # the backlog assert r.table('backlog').count().run(b.conn) == 100 - tx_from_block = set([tx['id'] for tx in invalid_block['block']['transactions']]) + # NOTE: I'm still, I'm still tx from the block. + tx_from_block = set([tx.id for tx in invalid_block.transactions]) tx_from_backlog = set([tx['id'] for tx in list(r.table('backlog').run(b.conn))]) assert tx_from_block == tx_from_backlog diff --git a/tests/pipelines/test_stale_monitor.py b/tests/pipelines/test_stale_monitor.py index f6cb4a0b..511b0492 100644 --- a/tests/pipelines/test_stale_monitor.py +++ b/tests/pipelines/test_stale_monitor.py @@ -9,8 +9,9 @@ import os def test_get_stale(b, user_vk): - tx = b.create_transaction(b.me, user_vk, None, 'CREATE') - tx = b.sign_transaction(tx, b.me_private) + from bigchaindb.models import Transaction + tx = Transaction.create([b.me], [user_vk]) + tx = tx.sign([b.me_private]) b.write_transaction(tx, durability='hard') stm = stale.StaleTransactionMonitor(timeout=0.001, @@ -20,22 +21,23 @@ def test_get_stale(b, user_vk): for _tx in tx_stale: _tx.pop('assignee') _tx.pop('assignment_timestamp') - assert tx == _tx + assert tx.to_dict() == _tx def test_reassign_transactions(b, user_vk): + from bigchaindb.models import Transaction # test with single node - tx = b.create_transaction(b.me, user_vk, None, 'CREATE') - tx = b.sign_transaction(tx, b.me_private) + tx = Transaction.create([b.me], [user_vk]) + tx = tx.sign([b.me_private]) b.write_transaction(tx, durability='hard') stm = stale.StaleTransactionMonitor(timeout=0.001, backlog_reassign_delay=0.001) - stm.reassign_transactions(tx) + stm.reassign_transactions(tx.to_dict()) # test with federation - tx = b.create_transaction(b.me, user_vk, None, 'CREATE') - tx = b.sign_transaction(tx, b.me_private) + tx = Transaction.create([b.me], [user_vk]) + tx = tx.sign([b.me_private]) b.write_transaction(tx, durability='hard') stm = stale.StaleTransactionMonitor(timeout=0.001, @@ -49,8 +51,8 @@ def test_reassign_transactions(b, user_vk): assert reassigned_tx['assignee'] != tx['assignee'] # test with node not in federation - tx = b.create_transaction(b.me, user_vk, None, 'CREATE') - tx = b.sign_transaction(tx, b.me_private) + tx = Transaction.create([b.me], [user_vk]) + tx = tx.sign([b.me_private]).to_dict() tx.update({'assignee': 'lol'}) tx.update({'assignment_timestamp': time.time()}) r.table('backlog').insert(tx, durability='hard').run(b.conn) @@ -61,6 +63,7 @@ def test_reassign_transactions(b, user_vk): def test_full_pipeline(user_vk): + from bigchaindb.models import Transaction CONFIG = { 'database': { 'name': 'bigchain_test_{}'.format(os.getpid()) @@ -77,13 +80,18 @@ def test_full_pipeline(user_vk): outpipe = Pipe() original_txs = {} + original_txc = [] for i in range(100): - tx = b.create_transaction(b.me, user_vk, None, 'CREATE') - tx = b.sign_transaction(tx, b.me_private) + # FIXME Notice the payload. This is only to make sure that the + # transactions hashes are unique. See + # https://github.com/bigchaindb/bigchaindb-common/issues/21 + tx = Transaction.create([b.me], [user_vk], payload={'i': i}) + tx = tx.sign([b.me_private]) + original_txc.append(tx.to_dict()) b.write_transaction(tx) - original_txs[tx['id']] = r.table('backlog').get(tx['id']).run(b.conn) + original_txs[tx.id] = r.table('backlog').get(tx.id).run(b.conn) assert r.table('backlog').count().run(b.conn) == 100 diff --git a/tests/pipelines/test_vote.py b/tests/pipelines/test_vote.py index d3d4fa2e..4ecd69a2 100644 --- a/tests/pipelines/test_vote.py +++ b/tests/pipelines/test_vote.py @@ -1,15 +1,14 @@ from unittest.mock import patch + import rethinkdb as r from multipipes import Pipe, Pipeline -from bigchaindb import util -from bigchaindb import crypto - def dummy_tx(b): - tx = b.create_transaction(b.me, b.me, None, 'CREATE') - tx_signed = b.sign_transaction(tx, b.me_private) - return tx_signed + from bigchaindb.models import Transaction + tx = Transaction.create([b.me], [b.me]) + tx = tx.sign([b.me_private]) + return tx def dummy_block(b): @@ -18,34 +17,40 @@ def dummy_block(b): def test_vote_creation_valid(b): + from bigchaindb_common import crypto + from bigchaindb_common.util import serialize + # create valid block block = dummy_block(b) # retrieve vote - vote = b.vote(block['id'], 'abc', True) + vote = b.vote(block.id, 'abc', True) # assert vote is correct - assert vote['vote']['voting_for_block'] == block['id'] + assert vote['vote']['voting_for_block'] == block.id assert vote['vote']['previous_block'] == 'abc' assert vote['vote']['is_block_valid'] is True assert vote['vote']['invalid_reason'] is None assert vote['node_pubkey'] == b.me - assert crypto.VerifyingKey(b.me).verify(util.serialize(vote['vote']), + assert crypto.VerifyingKey(b.me).verify(serialize(vote['vote']), vote['signature']) is True def test_vote_creation_invalid(b): + from bigchaindb_common import crypto + from bigchaindb_common.util import serialize + # create valid block block = dummy_block(b) # retrieve vote - vote = b.vote(block['id'], 'abc', False) + vote = b.vote(block.id, 'abc', False) # assert vote is correct - assert vote['vote']['voting_for_block'] == block['id'] + assert vote['vote']['voting_for_block'] == block.id assert vote['vote']['previous_block'] == 'abc' assert vote['vote']['is_block_valid'] is False assert vote['vote']['invalid_reason'] is None assert vote['node_pubkey'] == b.me - assert crypto.VerifyingKey(b.me).verify(util.serialize(vote['vote']), + assert crypto.VerifyingKey(b.me).verify(serialize(vote['vote']), vote['signature']) is True @@ -55,7 +60,7 @@ def test_vote_ungroup_returns_a_set_of_results(b): b.create_genesis_block() block = dummy_block(b) vote_obj = vote.Vote() - txs = list(vote_obj.ungroup(block, True)) + txs = list(vote_obj.ungroup(block.id, block.transactions)) assert len(txs) == 10 @@ -68,19 +73,53 @@ def test_vote_validate_block(b): block = b.create_block([tx]) vote_obj = vote.Vote() - validation = vote_obj.validate_block(block) - assert validation == (block, True) + validation = vote_obj.validate_block(block.to_dict()) + assert validation[0] == block.id + for tx1, tx2 in zip(validation[1], block.transactions): + assert tx1 == tx2 block = b.create_block([tx]) - block['block']['id'] = 'this-is-not-a-valid-hash' + # NOTE: Setting a blocks signature to `None` invalidates it. + block.signature = None vote_obj = vote.Vote() - validation = vote_obj.validate_block(block) - assert validation == (block, False) + validation = vote_obj.validate_block(block.to_dict()) + assert validation[0] == block.id + for tx1, tx2 in zip(validation[1], [vote_obj.invalid_dummy_tx]): + assert tx1 == tx2 + + +def test_validate_block_with_invalid_id(b): + from bigchaindb.pipelines import vote + + b.create_genesis_block() + tx = dummy_tx(b) + block = b.create_block([tx]).to_dict() + block['id'] = 'an invalid id' + + vote_obj = vote.Vote() + block_id, invalid_dummy_tx = vote_obj.validate_block(block) + assert block_id == block['id'] + assert invalid_dummy_tx == [vote_obj.invalid_dummy_tx] + + +def test_validate_block_with_invalid_signature(b): + from bigchaindb.pipelines import vote + + b.create_genesis_block() + tx = dummy_tx(b) + block = b.create_block([tx]).to_dict() + block['signature'] = 'an invalid signature' + + vote_obj = vote.Vote() + block_id, invalid_dummy_tx = vote_obj.validate_block(block) + assert block_id == block['id'] + assert invalid_dummy_tx == [vote_obj.invalid_dummy_tx] def test_vote_validate_transaction(b): from bigchaindb.pipelines import vote + from bigchaindb.models import Transaction b.create_genesis_block() tx = dummy_tx(b) @@ -88,7 +127,8 @@ def test_vote_validate_transaction(b): validation = vote_obj.validate_tx(tx, 123, 1) assert validation == (True, 123, 1) - tx['id'] = 'a' * 64 + # NOTE: Submit unsigned transaction to `validate_tx` yields `False`. + tx = Transaction.create([b.me], [b.me]) validation = vote_obj.validate_tx(tx, 456, 10) assert validation == (False, 456, 10) @@ -102,32 +142,34 @@ def test_vote_accumulates_transactions(b): for _ in range(10): tx = dummy_tx(b) + tx = tx validation = vote_obj.validate_tx(tx, 123, 1) assert validation == (True, 123, 1) - tx['id'] = 'a' * 64 + tx.fulfillments[0].fulfillment.signature = None validation = vote_obj.validate_tx(tx, 456, 10) assert validation == (False, 456, 10) def test_valid_block_voting_sequential(b, monkeypatch): + from bigchaindb_common import crypto, util from bigchaindb.pipelines import vote - monkeypatch.setattr(util, 'timestamp', lambda: '1') + monkeypatch.setattr('time.time', lambda: 1) genesis = b.create_genesis_block() vote_obj = vote.Vote() block = dummy_block(b) - for tx, block_id, num_tx in vote_obj.ungroup(block, True): + for tx, block_id, num_tx in vote_obj.ungroup(block.id, block.transactions): last_vote = vote_obj.vote(*vote_obj.validate_tx(tx, block_id, num_tx)) vote_obj.write_vote(last_vote) - vote_rs = r.table('votes').get_all([block['id'], b.me], + vote_rs = r.table('votes').get_all([block.id, b.me], index='block_and_voter').run(b.conn) vote_doc = vote_rs.next() - assert vote_doc['vote'] == {'voting_for_block': block['id'], - 'previous_block': genesis['id'], + assert vote_doc['vote'] == {'voting_for_block': block.id, + 'previous_block': genesis.id, 'is_block_valid': True, 'invalid_reason': None, 'timestamp': '1'} @@ -138,29 +180,30 @@ def test_valid_block_voting_sequential(b, monkeypatch): def test_valid_block_voting_multiprocessing(b, monkeypatch): + from bigchaindb_common import crypto, util from bigchaindb.pipelines import vote inpipe = Pipe() outpipe = Pipe() - monkeypatch.setattr(util, 'timestamp', lambda: '1') + monkeypatch.setattr('time.time', lambda: 1) genesis = b.create_genesis_block() vote_pipeline = vote.create_pipeline() vote_pipeline.setup(indata=inpipe, outdata=outpipe) block = dummy_block(b) - inpipe.put(block) + inpipe.put(block.to_dict()) vote_pipeline.start() vote_out = outpipe.get() vote_pipeline.terminate() - vote_rs = r.table('votes').get_all([block['id'], b.me], + vote_rs = r.table('votes').get_all([block.id, b.me], index='block_and_voter').run(b.conn) vote_doc = vote_rs.next() assert vote_out['vote'] == vote_doc['vote'] - assert vote_doc['vote'] == {'voting_for_block': block['id'], - 'previous_block': genesis['id'], + assert vote_doc['vote'] == {'voting_for_block': block.id, + 'previous_block': genesis.id, 'is_block_valid': True, 'invalid_reason': None, 'timestamp': '1'} @@ -171,17 +214,19 @@ def test_valid_block_voting_multiprocessing(b, monkeypatch): def test_valid_block_voting_with_create_transaction(b, monkeypatch): + from bigchaindb_common import crypto, util + from bigchaindb.models import Transaction from bigchaindb.pipelines import vote genesis = b.create_genesis_block() # create a `CREATE` transaction test_user_priv, test_user_pub = crypto.generate_key_pair() - tx = b.create_transaction(b.me, test_user_pub, None, 'CREATE') - tx_signed = b.sign_transaction(tx, b.me_private) + tx = Transaction.create([b.me], [test_user_pub]) + tx = tx.sign([b.me_private]) - monkeypatch.setattr(util, 'timestamp', lambda: '1') - block = b.create_block([tx_signed]) + monkeypatch.setattr('time.time', lambda: 1) + block = b.create_block([tx]) inpipe = Pipe() outpipe = Pipe() @@ -189,17 +234,17 @@ def test_valid_block_voting_with_create_transaction(b, monkeypatch): vote_pipeline = vote.create_pipeline() vote_pipeline.setup(indata=inpipe, outdata=outpipe) - inpipe.put(block) + inpipe.put(block.to_dict()) vote_pipeline.start() vote_out = outpipe.get() vote_pipeline.terminate() - vote_rs = r.table('votes').get_all([block['id'], b.me], + vote_rs = r.table('votes').get_all([block.id, b.me], index='block_and_voter').run(b.conn) vote_doc = vote_rs.next() assert vote_out['vote'] == vote_doc['vote'] - assert vote_doc['vote'] == {'voting_for_block': block['id'], - 'previous_block': genesis['id'], + assert vote_doc['vote'] == {'voting_for_block': block.id, + 'previous_block': genesis.id, 'is_block_valid': True, 'invalid_reason': None, 'timestamp': '1'} @@ -210,27 +255,28 @@ def test_valid_block_voting_with_create_transaction(b, monkeypatch): def test_valid_block_voting_with_transfer_transactions(monkeypatch, b): + from bigchaindb_common import crypto, util + from bigchaindb.models import Transaction from bigchaindb.pipelines import vote genesis = b.create_genesis_block() # create a `CREATE` transaction test_user_priv, test_user_pub = crypto.generate_key_pair() - tx = b.create_transaction(b.me, test_user_pub, None, 'CREATE') - tx_signed = b.sign_transaction(tx, b.me_private) + tx = Transaction.create([b.me], [test_user_pub]) + tx = tx.sign([b.me_private]) - monkeypatch.setattr(util, 'timestamp', lambda: '1') - block = b.create_block([tx_signed]) + monkeypatch.setattr('time.time', lambda: 1) + block = b.create_block([tx]) b.write_block(block, durability='hard') # create a `TRANSFER` transaction test_user2_priv, test_user2_pub = crypto.generate_key_pair() - tx2 = b.create_transaction(test_user_pub, test_user2_pub, - {'txid': tx['id'], 'cid': 0}, 'TRANSFER') - tx2_signed = b.sign_transaction(tx2, test_user_priv) + tx2 = Transaction.transfer(tx.to_inputs(), [test_user2_pub]) + tx2 = tx2.sign([test_user_priv]) - monkeypatch.setattr(util, 'timestamp', lambda: '2') - block2 = b.create_block([tx2_signed]) + monkeypatch.setattr('time.time', lambda: 2) + block2 = b.create_block([tx2]) b.write_block(block2, durability='hard') inpipe = Pipe() @@ -239,19 +285,19 @@ def test_valid_block_voting_with_transfer_transactions(monkeypatch, b): vote_pipeline = vote.create_pipeline() vote_pipeline.setup(indata=inpipe, outdata=outpipe) - inpipe.put(block) - inpipe.put(block2) + inpipe.put(block.to_dict()) + inpipe.put(block2.to_dict()) vote_pipeline.start() vote_out = outpipe.get() vote2_out = outpipe.get() vote_pipeline.terminate() - vote_rs = r.table('votes').get_all([block['id'], b.me], + vote_rs = r.table('votes').get_all([block.id, b.me], index='block_and_voter').run(b.conn) vote_doc = vote_rs.next() assert vote_out['vote'] == vote_doc['vote'] - assert vote_doc['vote'] == {'voting_for_block': block['id'], - 'previous_block': genesis['id'], + assert vote_doc['vote'] == {'voting_for_block': block.id, + 'previous_block': genesis.id, 'is_block_valid': True, 'invalid_reason': None, 'timestamp': '2'} @@ -260,12 +306,12 @@ def test_valid_block_voting_with_transfer_transactions(monkeypatch, b): assert crypto.VerifyingKey(b.me).verify(util.serialize(vote_doc['vote']), vote_doc['signature']) is True - vote2_rs = r.table('votes').get_all([block2['id'], b.me], + vote2_rs = r.table('votes').get_all([block2.id, b.me], index='block_and_voter').run(b.conn) vote2_doc = vote2_rs.next() assert vote2_out['vote'] == vote2_doc['vote'] - assert vote2_doc['vote'] == {'voting_for_block': block2['id'], - 'previous_block': block['id'], + assert vote2_doc['vote'] == {'voting_for_block': block2.id, + 'previous_block': block.id, 'is_block_valid': True, 'invalid_reason': None, 'timestamp': '2'} @@ -275,19 +321,61 @@ def test_valid_block_voting_with_transfer_transactions(monkeypatch, b): vote2_doc['signature']) is True -def test_invalid_tx_in_block_voting(monkeypatch, b, user_vk): +def test_unsigned_tx_in_block_voting(monkeypatch, b, user_vk): + from bigchaindb_common import crypto, util + from bigchaindb.models import Transaction from bigchaindb.pipelines import vote inpipe = Pipe() outpipe = Pipe() - monkeypatch.setattr(util, 'timestamp', lambda: '1') + monkeypatch.setattr('time.time', lambda: 1) genesis = b.create_genesis_block() vote_pipeline = vote.create_pipeline() vote_pipeline.setup(indata=inpipe, outdata=outpipe) - block = dummy_block(b) - block['block']['transactions'][0]['id'] = 'abc' + # NOTE: `tx` is invalid, because it wasn't signed. + tx = Transaction.create([b.me], [b.me]) + block = b.create_block([tx]) + + inpipe.put(block.to_dict()) + vote_pipeline.start() + vote_out = outpipe.get() + vote_pipeline.terminate() + + vote_rs = r.table('votes').get_all([block.id, b.me], + index='block_and_voter').run(b.conn) + vote_doc = vote_rs.next() + assert vote_out['vote'] == vote_doc['vote'] + assert vote_doc['vote'] == {'voting_for_block': block.id, + 'previous_block': genesis.id, + 'is_block_valid': False, + 'invalid_reason': None, + 'timestamp': '1'} + + assert vote_doc['node_pubkey'] == b.me + assert crypto.VerifyingKey(b.me).verify(util.serialize(vote_doc['vote']), + vote_doc['signature']) is True + + +def test_invalid_id_tx_in_block_voting(monkeypatch, b, user_vk): + from bigchaindb_common import crypto, util + from bigchaindb.models import Transaction + from bigchaindb.pipelines import vote + + inpipe = Pipe() + outpipe = Pipe() + + monkeypatch.setattr('time.time', lambda: 1) + genesis = b.create_genesis_block() + vote_pipeline = vote.create_pipeline() + 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 = tx.sign([b.me_private]) + block = b.create_block([tx]).to_dict() + block['block']['transactions'][0]['id'] = 'an invalid tx id' inpipe.put(block) vote_pipeline.start() @@ -299,7 +387,46 @@ def test_invalid_tx_in_block_voting(monkeypatch, b, user_vk): vote_doc = vote_rs.next() assert vote_out['vote'] == vote_doc['vote'] assert vote_doc['vote'] == {'voting_for_block': block['id'], - 'previous_block': genesis['id'], + 'previous_block': genesis.id, + 'is_block_valid': False, + 'invalid_reason': None, + 'timestamp': '1'} + + assert vote_doc['node_pubkey'] == b.me + assert crypto.VerifyingKey(b.me).verify(util.serialize(vote_doc['vote']), + vote_doc['signature']) is True + + +def test_invalid_content_in_tx_in_block_voting(monkeypatch, b, user_vk): + from bigchaindb_common import crypto, util + from bigchaindb.models import Transaction + from bigchaindb.pipelines import vote + + inpipe = Pipe() + outpipe = Pipe() + + monkeypatch.setattr('time.time', lambda: 1) + genesis = b.create_genesis_block() + vote_pipeline = vote.create_pipeline() + 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 = tx.sign([b.me_private]) + block = b.create_block([tx]).to_dict() + block['block']['transactions'][0]['id'] = 'an invalid tx id' + + inpipe.put(block) + vote_pipeline.start() + vote_out = outpipe.get() + vote_pipeline.terminate() + + vote_rs = r.table('votes').get_all([block['id'], b.me], + index='block_and_voter').run(b.conn) + vote_doc = vote_rs.next() + assert vote_out['vote'] == vote_doc['vote'] + assert vote_doc['vote'] == {'voting_for_block': block['id'], + 'previous_block': genesis.id, 'is_block_valid': False, 'invalid_reason': None, 'timestamp': '1'} @@ -310,17 +437,18 @@ def test_invalid_tx_in_block_voting(monkeypatch, b, user_vk): def test_invalid_block_voting(monkeypatch, b, user_vk): + from bigchaindb_common import crypto, util from bigchaindb.pipelines import vote inpipe = Pipe() outpipe = Pipe() - monkeypatch.setattr(util, 'timestamp', lambda: '1') + monkeypatch.setattr('time.time', lambda: 1) genesis = b.create_genesis_block() vote_pipeline = vote.create_pipeline() vote_pipeline.setup(indata=inpipe, outdata=outpipe) - block = dummy_block(b) + block = dummy_block(b).to_dict() block['block']['id'] = 'this-is-not-a-valid-hash' inpipe.put(block) @@ -333,7 +461,7 @@ def test_invalid_block_voting(monkeypatch, b, user_vk): vote_doc = vote_rs.next() assert vote_out['vote'] == vote_doc['vote'] assert vote_doc['vote'] == {'voting_for_block': block['id'], - 'previous_block': genesis['id'], + 'previous_block': genesis.id, 'is_block_valid': False, 'invalid_reason': None, 'timestamp': '1'} @@ -348,13 +476,15 @@ def test_voter_considers_unvoted_blocks_when_single_node(monkeypatch, b): outpipe = Pipe() - monkeypatch.setattr(util, 'timestamp', lambda: '1') + monkeypatch.setattr('time.time', lambda: 1) b.create_genesis_block() # insert blocks in the database while the voter process is not listening # (these blocks won't appear in the changefeed) + monkeypatch.setattr('time.time', lambda: 2) block_1 = dummy_block(b) b.write_block(block_1, durability='hard') + monkeypatch.setattr('time.time', lambda: 3) block_2 = dummy_block(b) b.write_block(block_2, durability='hard') @@ -368,6 +498,7 @@ def test_voter_considers_unvoted_blocks_when_single_node(monkeypatch, b): outpipe.get() # create a new block that will appear in the changefeed + monkeypatch.setattr('time.time', lambda: 4) block_3 = dummy_block(b) b.write_block(block_3, durability='hard') @@ -376,7 +507,7 @@ def test_voter_considers_unvoted_blocks_when_single_node(monkeypatch, b): vote_pipeline.terminate() - # retrive blocks from bigchain + # retrieve blocks from bigchain blocks = list(r.table('bigchain') .order_by(r.asc((r.row['block']['timestamp']))) .run(b.conn)) @@ -398,14 +529,14 @@ def test_voter_chains_blocks_with_the_previous_ones(monkeypatch, b): outpipe = Pipe() - monkeypatch.setattr(util, 'timestamp', lambda: '1') + monkeypatch.setattr('time.time', lambda: 1) b.create_genesis_block() - monkeypatch.setattr(util, 'timestamp', lambda: '2') + monkeypatch.setattr('time.time', lambda: 2) block_1 = dummy_block(b) b.write_block(block_1, durability='hard') - monkeypatch.setattr(util, 'timestamp', lambda: '3') + monkeypatch.setattr('time.time', lambda: 3) block_2 = dummy_block(b) b.write_block(block_2, durability='hard') @@ -437,11 +568,12 @@ def test_voter_checks_for_previous_vote(monkeypatch, b): inpipe = Pipe() outpipe = Pipe() - monkeypatch.setattr(util, 'timestamp', lambda: '1') + monkeypatch.setattr('time.time', lambda: 1) b.create_genesis_block() + monkeypatch.setattr('time.time', lambda: 2) block_1 = dummy_block(b) - inpipe.put(block_1) + inpipe.put(block_1.to_dict()) assert r.table('votes').count().run(b.conn) == 0 @@ -453,10 +585,12 @@ def test_voter_checks_for_previous_vote(monkeypatch, b): outpipe.get() # queue block for voting AGAIN - inpipe.put(block_1) + monkeypatch.setattr('time.time', lambda: 3) + inpipe.put(block_1.to_dict()) # queue another block - inpipe.put(dummy_block(b)) + monkeypatch.setattr('time.time', lambda: 4) + inpipe.put(dummy_block(b).to_dict()) # wait for the result of the new block outpipe.get() diff --git a/tests/test_client.py b/tests/test_client.py deleted file mode 100644 index e4b49dd6..00000000 --- a/tests/test_client.py +++ /dev/null @@ -1,74 +0,0 @@ -import pytest - - -@pytest.fixture -def client(): - from bigchaindb.client import temp_client - return temp_client() - - -@pytest.fixture -def mock_requests_post(monkeypatch): - class MockResponse: - def __init__(self, json): - self._json = json - - def json(self): - return self._json - - def mockreturn(*args, **kwargs): - return MockResponse(kwargs.get('json')) - - monkeypatch.setattr('requests.post', mockreturn) - -@pytest.fixture -def mock_bigchaindb_sign(monkeypatch): - def mockreturn(transaction, private_key, bigchain): - return transaction - - monkeypatch.setattr('bigchaindb.util.sign_tx', mockreturn) - - -def test_temp_client_returns_a_temp_client(): - from bigchaindb.client import temp_client - client = temp_client() - assert client.public_key - assert client.private_key - - -@pytest.mark.usefixtures('restore_config') -def test_client_can_create_assets(mock_requests_post, client): - from bigchaindb import util - - tx = client.create() - - # XXX: `CREATE` operations require the node that receives the transaction to modify the data in - # the transaction itself. - # `owner_before` will be overwritten with the public key of the node in the federation - # that will create the real transaction. `signature` will be overwritten with the new signature. - # Note that this scenario is ignored by this test. - assert tx['transaction']['fulfillments'][0]['owners_before'][0] == client.public_key - assert tx['transaction']['conditions'][0]['owners_after'][0] == client.public_key - assert tx['transaction']['fulfillments'][0]['input'] is None - - assert util.validate_fulfillments(tx) - - -def test_client_can_transfer_assets(mock_requests_post, mock_bigchaindb_sign, client): - tx = client.transfer(client.public_key, 123) - assert tx['transaction']['fulfillments'][0]['owners_before'][0] == client.public_key - assert tx['transaction']['conditions'][0]['owners_after'][0] == client.public_key - assert tx['transaction']['fulfillments'][0]['input'] == 123 - - -@pytest.mark.parametrize('pubkey,privkey', ( - (None, None), ('pubkey', None), (None, 'privkey'), -)) -def test_init_client_with_incomplete_keypair(pubkey, privkey, monkeypatch): - from bigchaindb import config - from bigchaindb.client import Client - from bigchaindb.exceptions import KeypairNotFoundException - keypair = {'public': pubkey, 'private': privkey} - monkeypatch.setitem(config, 'keypair', keypair) - with pytest.raises(KeypairNotFoundException): - Client() diff --git a/tests/test_commands.py b/tests/test_commands.py index 4efa8f96..6cb19d48 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -22,7 +22,7 @@ def mock_write_config(monkeypatch): @pytest.fixture def mock_db_init_with_existing_db(monkeypatch): from bigchaindb import db - from bigchaindb.exceptions import DatabaseAlreadyExists + from bigchaindb_common.exceptions import DatabaseAlreadyExists def mockreturn(): raise DatabaseAlreadyExists @@ -48,7 +48,7 @@ def mock_rethink_db_drop(monkeypatch): @pytest.fixture def mock_generate_key_pair(monkeypatch): - monkeypatch.setattr('bigchaindb.crypto.generate_key_pair', lambda: ('privkey', 'pubkey')) + monkeypatch.setattr('bigchaindb_common.crypto.generate_key_pair', lambda: ('privkey', 'pubkey')) @pytest.fixture @@ -283,15 +283,15 @@ def test_start_rethinkdb_returns_a_process_when_successful(mock_popen): @patch('subprocess.Popen') def test_start_rethinkdb_exits_when_cannot_start(mock_popen): - from bigchaindb import exceptions + from bigchaindb_common import exceptions from bigchaindb.commands import utils mock_popen.return_value = Mock(stdout=['Nopety nope']) with pytest.raises(exceptions.StartupError): utils.start_rethinkdb() -@patch('bigchaindb.crypto.generate_key_pair', return_value=('private_key', - 'public_key')) +@patch('bigchaindb_common.crypto.generate_key_pair', + return_value=('private_key', 'public_key')) def test_allow_temp_keypair_generates_one_on_the_fly(mock_gen_keypair, mock_processes_start, mock_db_init_with_existing_db): @@ -307,8 +307,8 @@ def test_allow_temp_keypair_generates_one_on_the_fly(mock_gen_keypair, assert bigchaindb.config['keypair']['public'] == 'public_key' -@patch('bigchaindb.crypto.generate_key_pair', return_value=('private_key', - 'public_key')) +@patch('bigchaindb_common.crypto.generate_key_pair', + return_value=('private_key', 'public_key')) def test_allow_temp_keypair_doesnt_override_if_keypair_found(mock_gen_keypair, mock_processes_start, mock_db_init_with_existing_db): @@ -415,3 +415,81 @@ def test_set_replicas_raises_exception(mock_log, monkeypatch, b): run_set_replicas(args) assert mock_log.called + + +@patch('argparse.ArgumentParser.parse_args') +@patch('bigchaindb.commands.utils.base_parser') +@patch('bigchaindb.commands.utils.start') +def test_calling_main(start_mock, base_parser_mock, parse_args_mock, + monkeypatch): + from bigchaindb.commands.bigchain import main + + argparser_mock = Mock() + parser = Mock() + subparsers = Mock() + subsubparsers = Mock() + subparsers.add_parser.return_value = subsubparsers + parser.add_subparsers.return_value = subparsers + argparser_mock.return_value = parser + monkeypatch.setattr('argparse.ArgumentParser', argparser_mock) + main() + + assert argparser_mock.called is True + assert parser.add_argument.called is True + parser.add_argument.assert_any_call('--dev-start-rethinkdb', + dest='start_rethinkdb', + action='store_true', + help='Run RethinkDB on start') + parser.add_subparsers.assert_called_with(title='Commands', + dest='command') + subparsers.add_parser.assert_any_call('configure', + help='Prepare the config file ' + 'and create the node keypair') + subparsers.add_parser.assert_any_call('show-config', + help='Show the current ' + 'configuration') + subparsers.add_parserassert_any_call('export-my-pubkey', + help="Export this node's public " + 'key') + subparsers.add_parser.assert_any_call('init', help='Init the database') + subparsers.add_parser.assert_any_call('drop', help='Drop the database') + subparsers.add_parser.assert_any_call('start', help='Start BigchainDB') + + subparsers.add_parser.assert_any_call('set-shards', + help='Configure number of shards') + + subsubparsers.add_argument.assert_any_call('num_shards', + metavar='num_shards', + type=int, default=1, + help='Number of shards') + + subparsers.add_parser.assert_any_call('set-replicas', + help='Configure number of replicas') + subsubparsers.add_argument.assert_any_call('num_replicas', + metavar='num_replicas', + type=int, default=1, + help='Number of replicas (i.e. ' + 'the replication factor)') + + subparsers.add_parser.assert_any_call('load', + help='Write transactions to the ' + 'backlog') + + subsubparsers.add_argument.assert_any_call('-m', '--multiprocess', + nargs='?', type=int, + default=False, + help='Spawn multiple processes ' + 'to run the command, if no ' + 'value is provided, the number ' + 'of processes is equal to the ' + 'number of cores of the host ' + 'machine') + subsubparsers.add_argument.assert_any_call('-c', '--count', + default=0, + type=int, + help='Number of transactions ' + 'to push. If the parameter -m ' + 'is set, the count is ' + 'distributed equally to all ' + 'the processes') + assert start_mock.called is True diff --git a/tests/test_config_utils.py b/tests/test_config_utils.py index 4d2b3a1e..5920ce27 100644 --- a/tests/test_config_utils.py +++ b/tests/test_config_utils.py @@ -4,7 +4,6 @@ from unittest.mock import mock_open, patch import pytest import bigchaindb -from bigchaindb import exceptions ORIGINAL_CONFIG = copy.deepcopy(bigchaindb._config) @@ -43,6 +42,7 @@ def test_bigchain_instance_is_initialized_when_conf_provided(): def test_bigchain_instance_raises_when_not_configured(monkeypatch): from bigchaindb import config_utils + from bigchaindb_common import exceptions assert 'CONFIGURED' not in bigchaindb.config # We need to disable ``bigchaindb.config_utils.autoconfigure`` to avoid reading @@ -53,36 +53,6 @@ def test_bigchain_instance_raises_when_not_configured(monkeypatch): bigchaindb.Bigchain() -def test_load_consensus_plugin_loads_default_rules_without_name(): - from bigchaindb import config_utils - from bigchaindb.consensus import BaseConsensusRules - - assert config_utils.load_consensus_plugin() == BaseConsensusRules - - -def test_load_consensus_plugin_raises_with_unknown_name(): - from pkg_resources import ResolutionError - from bigchaindb import config_utils - - with pytest.raises(ResolutionError): - config_utils.load_consensus_plugin('bogus') - - -def test_load_consensus_plugin_raises_with_invalid_subclass(monkeypatch): - # Monkeypatch entry_point.load to return something other than a - # ConsensusRules instance - from bigchaindb import config_utils - import time - monkeypatch.setattr(config_utils, - 'iter_entry_points', - lambda *args: [type('entry_point', (object), {'load': lambda: object})]) - - with pytest.raises(TypeError): - # Since the function is decorated with `lru_cache`, we need to - # "miss" the cache using a name that has not been used previously - config_utils.load_consensus_plugin(str(time.time())) - - def test_map_leafs_iterator(): from bigchaindb import config_utils @@ -180,7 +150,6 @@ def test_autoconfigure_read_both_from_file_and_env(monkeypatch): 'rate': 0.01, }, 'api_endpoint': 'http://localhost:9984/api/v1', - 'consensus_plugin': 'default', 'backlog_reassign_delay': 5 } @@ -232,8 +201,9 @@ def test_file_config(): def test_invalid_file_config(): - from bigchaindb.config_utils import file_config, CONFIG_DEFAULT_PATH - with patch('builtins.open', mock_open(read_data='{_INVALID_JSON_}')) as m: + from bigchaindb.config_utils import file_config + from bigchaindb_common import exceptions + with patch('builtins.open', mock_open(read_data='{_INVALID_JSON_}')): with pytest.raises(exceptions.ConfigurationError): file_config() diff --git a/tests/test_consensus.py b/tests/test_consensus.py index 8f8f9bcd..ef988726 100644 --- a/tests/test_consensus.py +++ b/tests/test_consensus.py @@ -1,15 +1,2 @@ -import pytest - - class TestBaseConsensusRules(object): - - def test_validate_transaction(self): - from bigchaindb.consensus import BaseConsensusRules - transaction = { - 'transaction': { - 'operation': None, - 'fulfillments': None, - }, - } - with pytest.raises(ValueError): - BaseConsensusRules.validate_transaction(None, transaction) + pass diff --git a/tests/test_core.py b/tests/test_core.py index 397158d0..6ad5c82b 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -19,7 +19,6 @@ def config(request, monkeypatch): }, 'keyring': [], 'CONFIGURED': True, - 'consensus_plugin': 'default', 'backlog_reassign_delay': 30 } @@ -52,7 +51,6 @@ def test_bigchain_class_initialization_with_parameters(config): 'public_key': 'white', 'private_key': 'black', 'keyring': ['key_one', 'key_two'], - 'consensus_plugin': 'default' } bigchain = Bigchain(**init_kwargs) assert bigchain.host == init_kwargs['host'] @@ -71,9 +69,9 @@ def test_get_blocks_status_containing_tx(monkeypatch): {'id': 1}, {'id': 2} ] monkeypatch.setattr( - Bigchain, 'search_block_election_on_index', lambda x, y, z: blocks) + Bigchain, 'search_block_election_on_index', lambda x, y: blocks) monkeypatch.setattr( - Bigchain, 'block_election_status', lambda x, y: Bigchain.BLOCK_VALID) + 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') @@ -82,7 +80,7 @@ def test_get_blocks_status_containing_tx(monkeypatch): def test_has_previous_vote(monkeypatch): from bigchaindb.core import Bigchain monkeypatch.setattr( - 'bigchaindb.util.verify_vote_signature', lambda block, vote: False) + 'bigchaindb.util.verify_vote_signature', lambda voters, vote: False) bigchain = Bigchain(public_key='pubkey', private_key='privkey') block = {'votes': ({'node_pubkey': 'pubkey'},)} with pytest.raises(Exception): @@ -98,6 +96,7 @@ def test_transaction_exists(monkeypatch, items, exists): assert bigchain.transaction_exists('txid') is exists +@pytest.mark.skip(reason='until tim decides') def test_write_transaction_no_sideffects(b): from rethinkdb.errors import ReqlOpFailedError transaction = {'id': 'abc'} diff --git a/tests/test_models.py b/tests/test_models.py new file mode 100644 index 00000000..7160d9a1 --- /dev/null +++ b/tests/test_models.py @@ -0,0 +1,178 @@ +from pytest import raises + + +class TestTransactionModel(object): + def test_validating_an_invalid_transaction(self, b): + from bigchaindb.models import Transaction + + tx = Transaction.create([b.me], [b.me]) + tx.operation = 'something invalid' + + with raises(TypeError): + tx.validate(b) + + tx.operation = 'CREATE' + tx.fulfillments = [] + with raises(ValueError): + tx.validate(b) + + +class TestBlockModel(object): + def test_block_initialization(self, monkeypatch): + from bigchaindb.models import Block + + monkeypatch.setattr('time.time', lambda: 1) + + block = Block() + assert block.transactions == [] + assert block.voters == [] + assert block.timestamp == '1' + assert block.node_pubkey is None + assert block.signature is None + + with raises(TypeError): + Block('not a list or None') + with raises(TypeError): + Block(None, 'valid node_pubkey', 'valid timestamp', + 'not a list or None') + + def test_block_serialization(self, b): + from bigchaindb_common.crypto import hash_data + from bigchaindb_common.util import gen_timestamp, serialize + from bigchaindb.models import Block, Transaction + + transactions = [Transaction.create([b.me], [b.me])] + timestamp = gen_timestamp() + voters = ['Qaaa', 'Qbbb'] + expected_block = { + 'timestamp': timestamp, + 'transactions': [tx.to_dict() for tx in transactions], + 'node_pubkey': b.me, + 'voters': voters, + } + expected = { + 'id': hash_data(serialize(expected_block)), + 'block': expected_block, + 'signature': None, + } + + block = Block(transactions, b.me, timestamp, voters) + + assert block.to_dict() == expected + + def test_block_invalid_serializaton(self): + from bigchaindb_common.exceptions import OperationError + from bigchaindb.models import Block + + block = Block([]) + with raises(OperationError): + block.to_dict() + + def test_block_deserialization(self, b): + from bigchaindb_common.crypto import hash_data + from bigchaindb_common.util import gen_timestamp, serialize + from bigchaindb.models import Block, Transaction + + transactions = [Transaction.create([b.me], [b.me])] + timestamp = gen_timestamp() + voters = ['Qaaa', 'Qbbb'] + expected = Block(transactions, b.me, timestamp, voters) + + block = { + 'timestamp': timestamp, + 'transactions': [tx.to_dict() for tx in transactions], + 'node_pubkey': b.me, + 'voters': voters, + } + + block_body = { + 'id': hash_data(serialize(block)), + 'block': block, + 'signature': None, + } + + assert expected == Block.from_dict(block_body) + + def test_block_invalid_id_deserialization(self, b): + from bigchaindb_common.exceptions import InvalidHash + from bigchaindb.models import Block + + block = { + 'id': 'an invalid id', + 'block': { + 'node_pubkey': b.me, + } + } + + with raises(InvalidHash): + Block.from_dict(block) + + def test_block_invalid_signature_deserialization(self, b): + from bigchaindb_common.crypto import hash_data + from bigchaindb_common.exceptions import InvalidSignature + from bigchaindb_common.util import gen_timestamp, serialize + from bigchaindb.models import Block, Transaction + + transactions = [Transaction.create([b.me], [b.me])] + timestamp = gen_timestamp() + voters = ['Qaaa', 'Qbbb'] + + block = { + 'timestamp': timestamp, + 'transactions': [tx.to_dict() for tx in transactions], + 'node_pubkey': b.me, + 'voters': voters, + } + + block_body = { + 'id': hash_data(serialize(block)), + 'block': block, + 'signature': 'an invalid signature', + } + + with raises(InvalidSignature): + Block.from_dict(block_body) + + def test_compare_blocks(self, b): + from bigchaindb.models import Block, Transaction + + transactions = [Transaction.create([b.me], [b.me])] + + assert Block() != 'invalid comparison' + assert Block(transactions) == Block(transactions) + + def test_sign_block(self, b): + from bigchaindb_common.crypto import SigningKey, VerifyingKey + from bigchaindb_common.util import gen_timestamp, serialize + from bigchaindb.models import Block, Transaction + + transactions = [Transaction.create([b.me], [b.me])] + timestamp = gen_timestamp() + voters = ['Qaaa', 'Qbbb'] + expected_block = { + 'timestamp': timestamp, + 'transactions': [tx.to_dict() for tx in transactions], + 'node_pubkey': b.me, + 'voters': voters, + } + expected_block_serialized = serialize(expected_block) + expected = SigningKey(b.me_private).sign(expected_block_serialized) + block = Block(transactions, b.me, timestamp, voters) + block = block.sign(b.me_private) + assert block.signature == expected + + verifying_key = VerifyingKey(b.me) + assert verifying_key.verify(expected_block_serialized, block.signature) + + def test_validate_already_voted_on_block(self, b, monkeypatch): + from unittest.mock import Mock + from bigchaindb.models import Transaction + + tx = Transaction.create([b.me], [b.me]) + block = b.create_block([tx]) + + has_previous_vote = Mock() + has_previous_vote.return_value = True + monkeypatch.setattr(b, 'has_previous_vote', has_previous_vote) + assert block == block.validate(b) + assert has_previous_vote.called is True diff --git a/tests/test_util.py b/tests/test_util.py index ae0b05ec..fe82c7ab 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -3,8 +3,6 @@ from unittest.mock import patch, call import pytest -from cryptoconditions import ThresholdSha256Fulfillment - @pytest.fixture def mock_queue(monkeypatch): @@ -29,17 +27,6 @@ def mock_queue(monkeypatch): return mockqueue -def test_transform_create(b, user_sk, user_vk): - from bigchaindb import util - tx = util.create_tx(user_vk, user_vk, None, 'CREATE') - tx = util.transform_create(tx) - tx = util.sign_tx(tx, b.me_private) - - assert tx['transaction']['fulfillments'][0]['owners_before'][0] == b.me - assert tx['transaction']['conditions'][0]['owners_after'][0] == user_vk - assert util.validate_fulfillments(tx) - - def test_empty_pool_is_populated_with_instances(mock_queue): from bigchaindb import util @@ -146,72 +133,6 @@ def test_process_group_instantiates_and_start_processes(mock_process): process.start.assert_called_with() -def test_create_tx_with_empty_inputs(): - from bigchaindb.util import create_tx - tx = create_tx(None, None, [], None) - assert 'id' in tx - assert 'transaction' in tx - assert 'version' in tx['transaction'] - assert 'fulfillments' in tx['transaction'] - assert 'conditions' in tx['transaction'] - assert 'operation' in tx['transaction'] - assert 'timestamp' in tx['transaction'] - assert 'data' in tx['transaction'] - assert len(tx['transaction']['fulfillments']) == 1 - assert tx['transaction']['fulfillments'][0] == { - 'owners_before': [], 'input': None, 'fulfillment': None, 'fid': 0} - - -def test_fulfill_threshold_signature_fulfillment_pubkey_notfound(monkeypatch): - from bigchaindb.exceptions import KeypairMismatchException - from bigchaindb.util import fulfill_threshold_signature_fulfillment - monkeypatch.setattr( - ThresholdSha256Fulfillment, - 'get_subcondition_from_vk', - lambda x, y: [] - ) - fulfillment = {'owners_before': (None,)} - parsed_fulfillment = ThresholdSha256Fulfillment() - with pytest.raises(KeypairMismatchException): - fulfill_threshold_signature_fulfillment( - fulfillment, parsed_fulfillment, None, None) - - -def test_fulfill_threshold_signature_fulfillment_wrong_privkeys(monkeypatch): - from bigchaindb.exceptions import KeypairMismatchException - from bigchaindb.util import fulfill_threshold_signature_fulfillment - monkeypatch.setattr( - ThresholdSha256Fulfillment, - 'get_subcondition_from_vk', - lambda x, y: (None,) - ) - fulfillment = {'owners_before': ('alice-pub-key',)} - parsed_fulfillment = ThresholdSha256Fulfillment() - with pytest.raises(KeypairMismatchException): - fulfill_threshold_signature_fulfillment( - fulfillment, parsed_fulfillment, None, {}) - - -def test_check_hash_and_signature_invalid_hash(monkeypatch): - from bigchaindb.exceptions import InvalidHash - from bigchaindb.util import check_hash_and_signature - transaction = {'id': 'txid'} - monkeypatch.setattr('bigchaindb.util.get_hash_data', lambda tx: 'txhash') - with pytest.raises(InvalidHash): - check_hash_and_signature(transaction) - - -def test_check_hash_and_signature_invalid_signature(monkeypatch): - from bigchaindb.exceptions import InvalidSignature - from bigchaindb.util import check_hash_and_signature - transaction = {'id': 'txid'} - monkeypatch.setattr('bigchaindb.util.get_hash_data', lambda tx: 'txid') - monkeypatch.setattr( - 'bigchaindb.util.validate_fulfillments', lambda tx: False) - with pytest.raises(InvalidSignature): - check_hash_and_signature(transaction) - - def test_is_genesis_block_returns_true_if_genesis(b): from bigchaindb.util import is_genesis_block genesis_block = b.prepare_genesis_block() diff --git a/tests/web/conftest.py b/tests/web/conftest.py index d0455798..db5583e7 100644 --- a/tests/web/conftest.py +++ b/tests/web/conftest.py @@ -30,6 +30,8 @@ def app(request, node_config): @pytest.fixture +# NOTE: In order to have a database setup as well as the `input` fixture, +# we have to proxy `db.conftest.input` here. +# TODO: If possible replace this function with something nicer. def inputs(user_vk): conftest.inputs(user_vk) - diff --git a/tests/web/test_basic_views.py b/tests/web/test_basic_views.py index c58a8716..829374cd 100644 --- a/tests/web/test_basic_views.py +++ b/tests/web/test_basic_views.py @@ -1,8 +1,7 @@ import json import pytest -from bigchaindb import crypto -from bigchaindb import util +from bigchaindb_common import crypto TX_ENDPOINT = '/api/v1/transactions/' @@ -11,14 +10,10 @@ TX_ENDPOINT = '/api/v1/transactions/' @pytest.mark.usefixtures('inputs') def test_get_transaction_endpoint(b, client, user_vk): input_tx = b.get_owned_ids(user_vk).pop() - tx = b.get_transaction(input_tx['txid']) - res = client.get(TX_ENDPOINT + input_tx['txid']) + tx = b.get_transaction(input_tx.txid) + res = client.get(TX_ENDPOINT + tx.id) + assert tx.to_dict() == res.json assert res.status_code == 200 - assert tx == res.json - - res = client.get(TX_ENDPOINT + input_tx['txid'] + '/') - assert res.status_code == 200 - assert tx == res.json @pytest.mark.usefixtures('inputs') @@ -38,57 +33,82 @@ def test_api_endpoint_shows_basic_info(client): def test_post_create_transaction_endpoint(b, client): - sk, vk = crypto.generate_key_pair() + from bigchaindb.models import Transaction + user_priv, user_pub = crypto.generate_key_pair() - tx = util.create_and_sign_tx(sk, vk, vk, None, 'CREATE') + tx = Transaction.create([user_pub], [user_pub]) + tx = tx.sign([user_priv]) + + res = client.post(TX_ENDPOINT, data=json.dumps(tx.to_dict())) + assert res.json['transaction']['fulfillments'][0]['owners_before'][0] == user_pub + assert res.json['transaction']['conditions'][0]['owners_after'][0] == user_pub + + +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 = tx.sign([user_priv]).to_dict() + tx['id'] = 'invalid id' res = client.post(TX_ENDPOINT, data=json.dumps(tx)) - assert res.json['transaction']['fulfillments'][0]['owners_before'][0] == b.me - assert res.json['transaction']['conditions'][0]['owners_after'][0] == vk + assert res.status_code == 400 -def test_post_create_transaction_endpoint_without_trailing_slash(b, client): - sk, vk = crypto.generate_key_pair() +def test_post_create_transaction_with_invalid_signature(b, client): + from bigchaindb.models import Transaction + user_priv, user_pub = crypto.generate_key_pair() - tx = util.create_and_sign_tx(sk, vk, vk, None, 'CREATE') + tx = Transaction.create([user_pub], [user_pub]) + tx = tx.sign([user_priv]).to_dict() + tx['transaction']['fulfillments'][0]['fulfillment'] = 'invalid signature' - res = client.post(TX_ENDPOINT[:-1], data=json.dumps(tx)) - assert res.json['transaction']['fulfillments'][0]['owners_before'][0] == b.me - assert res.json['transaction']['conditions'][0]['owners_after'][0] == vk + res = client.post(TX_ENDPOINT, data=json.dumps(tx)) + assert res.status_code == 400 @pytest.mark.usefixtures('inputs') def test_post_transfer_transaction_endpoint(b, client, user_vk, user_sk): sk, vk = crypto.generate_key_pair() - input_valid = b.get_owned_ids(user_vk).pop() + from bigchaindb.models import Transaction - transfer = util.create_and_sign_tx(user_sk, user_vk, vk, input_valid) - res = client.post(TX_ENDPOINT, data=json.dumps(transfer)) + user_priv, user_pub = crypto.generate_key_pair() + + 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]) + transfer_tx = transfer_tx.sign([user_sk]) + + res = client.post(TX_ENDPOINT, data=json.dumps(transfer_tx.to_dict())) assert res.json['transaction']['fulfillments'][0]['owners_before'][0] == user_vk - assert res.json['transaction']['conditions'][0]['owners_after'][0] == vk + assert res.json['transaction']['conditions'][0]['owners_after'][0] == user_pub @pytest.mark.usefixtures('inputs') def test_post_invalid_transfer_transaction_returns_400(b, client, user_vk, user_sk): - sk, vk = crypto.generate_key_pair() - input_valid = b.get_owned_ids(user_vk).pop() - transfer = b.create_transaction(user_vk, vk, input_valid, 'TRANSFER') - # transfer is not signed - res = client.post(TX_ENDPOINT, data=json.dumps(transfer)) + from bigchaindb.models import Transaction + user_priv, user_pub = crypto.generate_key_pair() + + 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]) + + res = client.post(TX_ENDPOINT, data=json.dumps(transfer_tx.to_dict())) assert res.status_code == 400 @pytest.mark.usefixtures('inputs') def test_get_transaction_status_endpoint(b, client, user_vk): input_tx = b.get_owned_ids(user_vk).pop() - tx, status = b.get_transaction(input_tx['txid'], include_status=True) - res = client.get(TX_ENDPOINT + input_tx['txid'] + "/status") + tx, status = b.get_transaction(input_tx.txid, include_status=True) + res = client.get(TX_ENDPOINT + input_tx.txid + "/status") assert status == res.json['status'] assert res.status_code == 200 - res = client.get(TX_ENDPOINT + input_tx['txid'] + "/status/") + res = client.get(TX_ENDPOINT + input_tx.txid + "/status/") assert status == res.json['status'] assert res.status_code == 200 @@ -100,4 +120,3 @@ def test_get_transaction_status_returns_404_if_not_found(client): res = client.get(TX_ENDPOINT + '123' + "/status/") assert res.status_code == 404 - diff --git a/tests/web/test_server.py b/tests/web/test_server.py index 874567d7..10720934 100644 --- a/tests/web/test_server.py +++ b/tests/web/test_server.py @@ -1,6 +1,3 @@ -import copy - - def test_settings(monkeypatch): import bigchaindb from bigchaindb.web import server @@ -10,4 +7,3 @@ def test_settings(monkeypatch): # for whatever reason the value is wrapped in a list # needs further investigation assert s.cfg.bind[0] == bigchaindb.config['server']['bind'] -