diff --git a/PYTHON_STYLE_GUIDE.md b/PYTHON_STYLE_GUIDE.md index d416ad65..dc7e68d7 100644 --- a/PYTHON_STYLE_GUIDE.md +++ b/PYTHON_STYLE_GUIDE.md @@ -20,7 +20,7 @@ PEP8 says some things about docstrings, but not what to put in them or how to st PEP8 has some [maximum line length guidelines](https://www.python.org/dev/peps/pep-0008/#id17), starting with "Limit all lines to a maximum of 79 characters" but "for flowing long blocks of text with fewer structural restrictions (docstrings or comments), the line length should be limited to 72 characters." -We discussed this at length, and it seems that the consensus is: try to keep line lengths less than 79/72 characters, unless you have a special situation where longer lines would improve readability. (The basic reason is that 79/72 works for everyone, and BigchainDB is an open source project.) +We discussed this at length, and it seems that the consensus is: _try_ to keep line lengths less than 79/72 characters, unless you have a special situation where longer lines would improve readability. (The basic reason is that 79/72 works for everyone, and BigchainDB is an open source project.) As a hard limit, keep all lines less than 119 characters (which is the width of GitHub code review). ### Single or Double Quotes? diff --git a/README.md b/README.md index 86a198e0..0fd70ad9 100644 --- a/README.md +++ b/README.md @@ -34,3 +34,4 @@ A scalable blockchain database. [The whitepaper](https://www.bigchaindb.com/whit * [Licenses](LICENSES.md) - open source & open content * [Imprint](https://www.bigchaindb.com/imprint/) * [Contact Us](https://www.bigchaindb.com/contact/) + diff --git a/ROADMAP.md b/ROADMAP.md index a3c65086..1c7752ed 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -2,20 +2,28 @@ ## BigchainDB Protocols * Validation of other nodes -* Byzantine fault tolerance +* Fault tolerance * Permissions framework -* Benchmarks (e.g. on transactions/second and latency) -* API/Wire protocol exposed by the BigchainDB dameon (HTTP or other). Eventually, the _only_ way for a client to communicate with a BigchainDB database will be via this API. * Protocol audits including security audits ## Implementation/Code * Node validation framework (inspect and agree or not with what the other nodes are doing) -* Federation management and monitoring/dashboard -* Packaging, dockerization, AWS image, etc. (i.e. easy deployment options) -* Drivers/SDKs for common client-side languages (e.g. Python, Ruby, JavaScript, Java) +* Federation management tools +* More tools for benchmarking a cluster +* Descriptions and results of more benchmarking tests +* AWS image and other easy deployment options +* Drivers/SDKs for more client-side languages (e.g. JavaScript, Ruby, Java) * ORM to better-decouple BigchainDB from its data store (will make it easy to try other databases) * Code audits including security audits ## Other/Future -* Multisig +* Byzantine fault tolerance * Better support for smart contract frameworks + +## Done/Past (i.e. was in the Roadmap) +* Packaging for PyPI (setup.py etc.) - [the latest version release can be found on PyPI](https://pypi.python.org/pypi/BigchainDB) +* Dockerization +* Monitoring/dashboard - initial vesion added in [Pull Request #72](https://github.com/bigchaindb/bigchaindb/pull/72) +* API/Wire protocol (RESTful HTTP API) - initial version added in [Pull Request #102](https://github.com/bigchaindb/bigchaindb/pull/102) +* Python driver/SDK - initial version added in [Pull Request #102](https://github.com/bigchaindb/bigchaindb/pull/102) +* Multisig support - initial version added in [Pull Request #107](https://github.com/bigchaindb/bigchaindb/pull/107) diff --git a/bigchaindb/__init__.py b/bigchaindb/__init__.py index b728e103..0a69c150 100644 --- a/bigchaindb/__init__.py +++ b/bigchaindb/__init__.py @@ -40,9 +40,10 @@ config = { 'port': e('BIGCHAIN_STATSD_PORT', default=8125), 'rate': e('BIGCHAIN_STATSD_SAMPLERATE', default=0.01) }, + 'api_endpoint': 'http://localhost:8008/api/v1', 'consensus_plugins': [ 'base' - ], + ] } # We need to maintain a backup copy of the original config dict in case @@ -50,4 +51,3 @@ config = { # for more info. _config = copy.deepcopy(config) from bigchaindb.core import Bigchain # noqa - diff --git a/bigchaindb/client.py b/bigchaindb/client.py new file mode 100644 index 00000000..0a9eed01 --- /dev/null +++ b/bigchaindb/client.py @@ -0,0 +1,97 @@ +import requests + +import bigchaindb +from bigchaindb import util +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): + """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 ECDSA secp256k1 curve. + private_key (str): the base58 encoded private key for the ECDSA secp256k1 curve. + host (str): hostname where the rethinkdb is running. + port (int): port in which rethinkb is running (usually 28015). + """ + + 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'] + + 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 = util.create_tx(self.public_key, self.public_key, None, operation='CREATE', payload=payload) + signed_tx = util.sign_tx(tx, self.private_key) + return self._push(signed_tx) + + def transfer(self, new_owner, tx_input, payload=None): + """Issue a transaction to transfer an asset. + + Args: + new_owner (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 = util.create_tx(self.public_key, new_owner, tx_input, operation='TRANSFER', payload=payload) + signed_tx = util.sign_tx(tx, 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='http://localhost:5000/api/v1') + diff --git a/bigchaindb/consensus/base.py b/bigchaindb/consensus/base.py index ab01d26e..ed3f05c9 100644 --- a/bigchaindb/consensus/base.py +++ b/bigchaindb/consensus/base.py @@ -1,4 +1,5 @@ import bigchaindb.exceptions as exceptions +from bigchaindb import util from bigchaindb.crypto import hash_data from bigchaindb.consensus import AbstractConsensusRules @@ -72,7 +73,7 @@ class ConsensusRules(AbstractConsensusRules): transaction['transaction']['input'])) # Check hash of the transaction - calculated_hash = hash_data(bigchain.serialize( + calculated_hash = hash_data(util.serialize( transaction['transaction'])) if calculated_hash != transaction['id']: raise exceptions.InvalidHash() @@ -101,7 +102,7 @@ class ConsensusRules(AbstractConsensusRules): """ # Check if current hash is correct - calculated_hash = hash_data(bigchain.serialize(block['block'])) + calculated_hash = hash_data(util.serialize(block['block'])) if calculated_hash != block['id']: raise exceptions.InvalidHash() diff --git a/bigchaindb/core.py b/bigchaindb/core.py index 7df5ed85..ffdffa9b 100644 --- a/bigchaindb/core.py +++ b/bigchaindb/core.py @@ -1,17 +1,17 @@ import rethinkdb as r -import time import random import json import rapidjson -from datetime import datetime import bigchaindb +from bigchaindb import util from bigchaindb import config_utils from bigchaindb import exceptions -from bigchaindb.crypto import hash_data, PublicKey, PrivateKey, generate_key_pair +from bigchaindb import crypto from bigchaindb.monitor import Monitor + monitor = Monitor() @@ -19,10 +19,6 @@ class GenesisBlockAlreadyExistsError(Exception): pass -class KeypairNotFoundException(Exception): - pass - - class Bigchain(object): """Bigchain API @@ -48,8 +44,8 @@ class Bigchain(object): public_key (str): the base58 encoded public key for the ECDSA secp256k1 curve. private_key (str): the base58 encoded private key for the ECDSA secp256k1 curve. keyring (list[str]): list of base58 encoded public keys of the federation nodes. - """ + config_utils.autoconfigure() self.host = host or bigchaindb.config['database']['host'] self.port = port or bigchaindb.config['database']['port'] @@ -60,7 +56,7 @@ class Bigchain(object): self.consensus_plugins = config_utils.get_plugins(consensus_plugins) if not self.me or not self.me_private: - raise KeypairNotFoundException() + raise exceptions.KeypairNotFoundException() self._conn = None @@ -77,101 +73,25 @@ class Bigchain(object): def create_transaction(self, current_owner, new_owner, tx_input, 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: - current_owner (str): base58 encoded public key of the current owner of the asset. - new_owner (str): base58 encoded public key of the new owner of the digital asset. - tx_input (str): 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``. + Refer to the documentation of ``bigchaindb.util.create_tx`` """ - data = None - if payload is not None: - if isinstance(payload, dict): - hash_payload = hash_data(self.serialize(payload)) - data = { - 'hash': hash_payload, - 'payload': payload - } - else: - raise TypeError('`payload` must be an dict instance') - hash_payload = hash_data(self.serialize(payload)) - data = { - 'hash': hash_payload, - 'payload': payload - } - - tx = { - 'current_owner': current_owner, - 'new_owner': new_owner, - 'input': tx_input, - 'operation': operation, - 'timestamp': self.timestamp(), - 'data': data - } - - # serialize and convert to bytes - tx_serialized = self.serialize(tx) - tx_hash = hash_data(tx_serialized) - - # create the transaction - transaction = { - 'id': tx_hash, - 'transaction': tx - } - - return transaction + return util.create_tx(current_owner, new_owner, tx_input, operation, payload) def sign_transaction(self, transaction, private_key): """Sign a transaction - A transaction signed with the `current_owner` corresponding private key. - - Args: - transaction (dict): transaction to sign. - private_key (str): base58 encoded private key to create a signature of the transaction. - - Returns: - dict: transaction with the `signature` field included. - + Refer to the documentation of ``bigchaindb.util.sign_tx`` """ - private_key = PrivateKey(private_key) - signature = private_key.sign(self.serialize(transaction)) - signed_transaction = transaction.copy() - signed_transaction.update({'signature': signature}) - return signed_transaction + + return util.sign_tx(transaction, private_key) def verify_signature(self, signed_transaction): - """Verify the signature of a transaction - - A valid transaction should have been signed `current_owner` corresponding private key. - - Args: - signed_transaction (dict): a transaction with the `signature` included. - - Returns: - bool: True if the signature is correct, False otherwise. + """Verify the signature of a transaction. + Refer to the documentation of ``bigchaindb.crypto.verify_signature`` """ + data = signed_transaction.copy() # if assignee field in the transaction, remove it @@ -180,11 +100,11 @@ class Bigchain(object): signature = data.pop('signature') public_key_base58 = signed_transaction['transaction']['current_owner'] - public_key = PublicKey(public_key_base58) - return public_key.verify(self.serialize(data), signature) + public_key = crypto.PublicKey(public_key_base58) + return public_key.verify(util.serialize(data), signature) @monitor.timer('write_transaction', rate=bigchaindb.config['statsd']['rate']) - def write_transaction(self, signed_transaction): + def write_transaction(self, signed_transaction, durability='soft'): """Write the transaction to bigchain. When first writing a transaction to the bigchain the transaction will be kept in a backlog until @@ -210,7 +130,7 @@ class Bigchain(object): signed_transaction.update({'assignee': assignee}) # write to the backlog - response = r.table('backlog').insert(signed_transaction, durability='soft').run(self.conn) + response = r.table('backlog').insert(signed_transaction, durability=durability).run(self.conn) return response # TODO: the same `txid` can be in two different blocks @@ -226,8 +146,8 @@ class Bigchain(object): A dict with the transaction details if the transaction was found. If no transaction with that `txid` was found it returns `None` - """ + response = r.table('bigchain').concat_map(lambda doc: doc['block']['transactions'])\ .filter(lambda transaction: transaction['id'] == txid).run(self.conn) @@ -257,8 +177,8 @@ class Bigchain(object): Returns: A list of transactions containing that payload. If no transaction exists with that payload it returns `None` - """ + cursor = r.table('bigchain')\ .get_all(payload_hash, index='payload_hash')\ .run(self.conn) @@ -277,7 +197,6 @@ class Bigchain(object): Returns: The transaction that used the `txid` as an input if it exists else it returns `None` - """ # checks if an input was already spent # checks if the bigchain has any transaction with input `transaction_id` @@ -303,8 +222,8 @@ class Bigchain(object): Returns: list: list of `txids` currently owned by `owner` - """ + response = r.table('bigchain')\ .concat_map(lambda doc: doc['block']['transactions'])\ .filter({'transaction': {'new_owner': owner}})\ @@ -347,8 +266,8 @@ class Bigchain(object): Returns: bool: `True` if the transaction is valid, `False` otherwise - """ + try: self.validate_transaction(transaction) return transaction @@ -368,20 +287,20 @@ class Bigchain(object): Returns: dict: created block. - """ + # Create the new block block = { - 'timestamp': self.timestamp(), + 'timestamp': util.timestamp(), 'transactions': validated_transactions, 'node_pubkey': self.me, 'voters': self.federation_nodes + [self.me] } # Calculate the hash of the new block - block_data = self.serialize(block) - block_hash = hash_data(block_data) - block_signature = PrivateKey(self.me_private).sign(block_data) + block_data = util.serialize(block) + block_hash = crypto.hash_data(block_data) + block_signature = crypto.PrivateKey(self.me_private).sign(block_data) block = { 'id': block_hash, @@ -403,7 +322,6 @@ class Bigchain(object): Returns: The block if the block is valid else it raises and exception describing the reason why the block is invalid. - """ # First run all of the plugin block validation logic @@ -430,8 +348,8 @@ class Bigchain(object): Returns: bool: `True` if the block is valid, `False` otherwise. - """ + try: self.validate_block(block) return True @@ -444,8 +362,8 @@ class Bigchain(object): Args: block (dict): block to write to bigchain. - """ + block_serialized = rapidjson.dumps(block) r.table('bigchain').insert(r.json(block_serialized), durability=durability).run(self.conn) @@ -492,18 +410,18 @@ class Bigchain(object): previous_block_id (str): The id of the previous block. decision (bool): Whether the block is valid or invalid. invalid_reason (Optional[str]): Reason the block is invalid - """ + vote = { 'voting_for_block': block['id'], 'previous_block': previous_block_id, 'is_block_valid': decision, 'invalid_reason': invalid_reason, - 'timestamp': self.timestamp() + 'timestamp': util.timestamp() } - vote_data = self.serialize(vote) - signature = PrivateKey(self.me_private).sign(vote_data) + vote_data = util.serialize(vote) + signature = crypto.PrivateKey(self.me_private).sign(vote_data) vote_signed = { 'node_pubkey': self.me, @@ -514,9 +432,8 @@ class Bigchain(object): return vote_signed def write_vote(self, block, vote, block_number): - """ - Write the vote to the database - """ + """Write the vote to the database.""" + update = {'votes': r.row['votes'].append(vote)} # We need to *not* override the existing block_number, if any @@ -530,9 +447,8 @@ class Bigchain(object): .run(self.conn) def get_last_voted_block(self): - """ - Returns the last block that this node voted on - """ + """Returns the last block that this node voted on.""" + # query bigchain for all blocks this node is a voter but didn't voted on last_voted = r.table('bigchain')\ .filter(r.row['block']['voters'].contains(self.me))\ @@ -551,9 +467,7 @@ class Bigchain(object): return last_voted[0] def get_unvoted_blocks(self): - """ - Return all the blocks that has not been voted by this node. - """ + """Return all the blocks that has not been voted by this node.""" unvoted = r.table('bigchain')\ .filter(lambda doc: doc['votes'].contains(lambda vote: vote['node_pubkey'] == self.me).not_())\ @@ -564,60 +478,3 @@ class Bigchain(object): unvoted.pop(0) return unvoted - - @staticmethod - def serialize(data): - """Static method used to serialize a dict into a JSON formatted string. - - This method 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 json.dumps(data, skipkeys=False, ensure_ascii=False, - separators=(',', ':'), sort_keys=True) - - @staticmethod - def deserialize(data): - """Static method used to 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 json.loads(data, encoding="utf-8") - - @staticmethod - def timestamp(): - """Static method to calculate a UTC timestamp with microsecond precision. - - Returns: - str: UTC timestamp. - - """ - dt = datetime.utcnow() - return "{0:.6f}".format(time.mktime(dt.timetuple()) + dt.microsecond / 1e6) - - @staticmethod - def generate_keys(): - """Generates a key pair. - - Returns: - tuple: `(private_key, public_key)`. ECDSA key pair using the secp256k1 curve encoded - in base58. - - """ - # generates and returns the keys serialized in hex - return generate_key_pair() diff --git a/bigchaindb/crypto.py b/bigchaindb/crypto.py index 8eedae65..bcbe0863 100644 --- a/bigchaindb/crypto.py +++ b/bigchaindb/crypto.py @@ -1,9 +1,9 @@ # Separate all crypto code so that we can easily test several implementations -import hashlib -import sha3 import binascii import base58 + +import sha3 import bitcoin from cryptography.hazmat.backends import default_backend @@ -148,4 +148,8 @@ def generate_key_pair(): def hash_data(data): - return hashlib.sha3_256(data.encode()).hexdigest() + """Hash the provided data using SHA3-256""" + + return sha3.sha3_256(data.encode()).hexdigest() + + diff --git a/bigchaindb/exceptions.py b/bigchaindb/exceptions.py index 684ae58c..0baa4ad2 100644 --- a/bigchaindb/exceptions.py +++ b/bigchaindb/exceptions.py @@ -25,3 +25,7 @@ class DatabaseAlreadyExists(Exception): 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""" + + diff --git a/bigchaindb/processes.py b/bigchaindb/processes.py index 003f23ba..c7cf09f0 100644 --- a/bigchaindb/processes.py +++ b/bigchaindb/processes.py @@ -6,6 +6,7 @@ import rethinkdb as r from bigchaindb import Bigchain from bigchaindb.voter import Voter from bigchaindb.block import Block +from bigchaindb.web import server logger = logging.getLogger(__name__) @@ -80,3 +81,9 @@ class Processes(object): logger.info('starting voter') p_voter.start() + + # start the web api + webapi = server.create_app() + p_webapi = mp.Process(name='webapi', target=webapi.run, kwargs={'host': 'localhost'}) + p_webapi.start() + diff --git a/bigchaindb/util.py b/bigchaindb/util.py index 42589f2a..d8ddc9ce 100644 --- a/bigchaindb/util.py +++ b/bigchaindb/util.py @@ -1,4 +1,12 @@ + +import json +import time import multiprocessing as mp +from datetime import datetime + +import bigchaindb +from bigchaindb import exceptions +from bigchaindb.crypto import PrivateKey, PublicKey, hash_data class ProcessGroup(object): @@ -22,3 +30,191 @@ class ProcessGroup(object): proc.start() self.processes.append(proc) + +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 json.dumps(data, skipkeys=False, ensure_ascii=False, + separators=(',', ':'), 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 json.loads(data, encoding="utf-8") + + +def timestamp(): + """Calculate a UTC timestamp with microsecond precision. + + Returns: + str: UTC timestamp. + + """ + dt = datetime.utcnow() + return "{0:.6f}".format(time.mktime(dt.timetuple()) + dt.microsecond / 1e6) + + +def create_tx(current_owner, new_owner, tx_input, 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: + current_owner (str): base58 encoded public key of the current owner of the asset. + new_owner (str): base58 encoded public key of the new owner of the digital asset. + tx_input (str): 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``. + """ + + data = None + if payload is not None: + if isinstance(payload, dict): + hash_payload = hash_data(serialize(payload)) + data = { + 'hash': hash_payload, + 'payload': payload + } + else: + raise TypeError('`payload` must be an dict instance') + + hash_payload = hash_data(serialize(payload)) + data = { + 'hash': hash_payload, + 'payload': payload + } + + tx = { + 'current_owner': current_owner, + 'new_owner': new_owner, + 'input': tx_input, + 'operation': operation, + 'timestamp': timestamp(), + 'data': data + } + + # serialize and convert to bytes + tx_serialized = serialize(tx) + tx_hash = hash_data(tx_serialized) + + # create the transaction + transaction = { + 'id': tx_hash, + 'transaction': tx + } + + return transaction + + +def sign_tx(transaction, private_key): + """Sign a transaction + + A transaction signed with the `current_owner` corresponding private key. + + Args: + transaction (dict): transaction to sign. + private_key (str): base58 encoded private key to create a signature of the transaction. + + Returns: + dict: transaction with the `signature` field included. + + """ + private_key = PrivateKey(private_key) + signature = private_key.sign(serialize(transaction)) + signed_transaction = transaction.copy() + signed_transaction.update({'signature': signature}) + return signed_transaction + + +def create_and_sign_tx(private_key, current_owner, new_owner, tx_input, operation='TRANSFER', payload=None): + tx = create_tx(current_owner, new_owner, tx_input, operation, payload) + return sign_tx(tx, private_key) + + +def check_hash_and_signature(transaction): + # Check hash of the transaction + calculated_hash = hash_data(serialize(transaction['transaction'])) + if calculated_hash != transaction['id']: + raise exceptions.InvalidHash() + + # Check signature + if not verify_signature(transaction): + raise exceptions.InvalidSignature() + + +def verify_signature(signed_transaction): + """Verify the signature of a transaction + + A valid transaction should have been signed `current_owner` corresponding private key. + + Args: + signed_transaction (dict): a transaction with the `signature` included. + + Returns: + bool: True if the signature is correct, False otherwise. + """ + + data = signed_transaction.copy() + + # if assignee field in the transaction, remove it + if 'assignee' in data: + data.pop('assignee') + + signature = data.pop('signature') + public_key_base58 = signed_transaction['transaction']['current_owner'] + public_key = PublicKey(public_key_base58) + return public_key.verify(serialize(data), 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['current_owner'], None, 'CREATE', payload=payload) + return new_tx + diff --git a/bigchaindb/web/__init__.py b/bigchaindb/web/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/bigchaindb/web/server.py b/bigchaindb/web/server.py new file mode 100644 index 00000000..a062e4ba --- /dev/null +++ b/bigchaindb/web/server.py @@ -0,0 +1,21 @@ +"""This module contains basic functions to instantiate the BigchainDB API. """ + +from flask import Flask + +from bigchaindb import Bigchain +from bigchaindb.web import views + + +def create_app(debug=False): + """Return an instance of the Flask application. + + Args: + debug (bool): a flag to activate the debug mode for the app (default: False). + """ + + app = Flask(__name__) + app.debug = debug + app.config['bigchain'] = Bigchain() + app.register_blueprint(views.basic_views, url_prefix='/api/v1') + return app + diff --git a/bigchaindb/web/views.py b/bigchaindb/web/views.py new file mode 100644 index 00000000..04d3992d --- /dev/null +++ b/bigchaindb/web/views.py @@ -0,0 +1,68 @@ +"""This module provides the blueprint for some basic API endpoints. + +For more information please refer to the documentation in Apiary: + - http://docs.bigchaindb.apiary.io/ +""" + +import flask +from flask import current_app, request, Blueprint + +from bigchaindb import util + + +basic_views = Blueprint('basic_views', __name__) + + +@basic_views.record +def get_bigchain(state): + bigchain = state.app.config.get('bigchain') + + if bigchain is None: + raise Exception('This blueprint expects you to provide ' + 'database access through `bigchain`') + + + +@basic_views.route('/transactions/') +def get_transaction(tx_id): + """API endpoint to get details about a transaction. + + Args: + tx_id (str): the id of the transaction. + + Return: + A JSON string containing the data about the transaction. + """ + + bigchain = current_app.config['bigchain'] + + tx = bigchain.get_transaction(tx_id) + return flask.jsonify(**tx) + + +@basic_views.route('/transactions/', methods=['POST']) +def create_transaction(): + """API endpoint to push transactions to the Federation. + + Return: + A JSON string containing the data about the transaction. + """ + bigchain = current_app.config['bigchain'] + + val = {} + + # `force` will try to format the body of the POST request even if the `content-type` header is not + # set to `application/json` + tx = request.get_json(force=True) + + if tx['transaction']['operation'] == 'CREATE': + tx = util.transform_create(tx) + tx = util.sign_tx(tx, bigchain.me_private) + + if not util.verify_signature(tx): + val['error'] = 'Invalid transaction signature' + + val = bigchain.write_transaction(tx) + + return flask.jsonify(**tx) + diff --git a/docs/requirements.txt b/docs/requirements.txt index 3ffbffed..9e6b6214 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,3 +1,4 @@ Sphinx==1.3.5 sphinxcontrib-napoleon==0.4.4 +sphinx-rtd-theme>=0.1.9 recommonmark diff --git a/docs/source/conf.py b/docs/source/conf.py index 3651eb79..04f749df 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -28,6 +28,9 @@ from recommonmark.parser import CommonMarkParser # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. +import sphinx_rtd_theme + + extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.intersphinx', @@ -119,7 +122,8 @@ todo_include_todos = False # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'alabaster' +html_theme = 'sphinx_rtd_theme' + # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the @@ -127,7 +131,7 @@ html_theme = 'alabaster' #html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. -#html_theme_path = [] +html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". diff --git a/docs/source/index.rst b/docs/source/index.rst index d15ff99b..6934f335 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -16,6 +16,7 @@ Table of Contents installing getting-started bigchaindb-cli + python-api-tutorial admin cryptography models diff --git a/docs/source/installing.md b/docs/source/installing.md index 9ff24298..3e38013f 100644 --- a/docs/source/installing.md +++ b/docs/source/installing.md @@ -27,12 +27,20 @@ If you don't already have it, then you should [install Python 3.4+](https://www. BigchainDB has some OS-level dependencies. In particular, you need to install the OS-level dependencies for the Python **cryptography** package. Instructions for installing those dependencies on your OS can be found in the [cryptography package documentation](https://cryptography.io/en/latest/installation/). -On Ubuntu 14.04, we found that the following was enough (YMMV): +On Ubuntu 14.04, we found that the following was enough: ```text $ sudo apt-get update $ sudo apt-get install libffi-dev g++ libssl-dev python3-dev ``` +On Fedora 23, we found that the following was enough (tested in February 2015): +```text +$ sudo dnf update +$ sudo dnf install libffi-devel gcc-c++ redhat-rpm-config python3-devel openssl-devel +``` + +(If you're using a version of Fedora before version 22, you may have to use `yum` instead of `dnf`.) + With OS-level dependencies installed, you can install BigchainDB with `pip` or from source. ### How to Install BigchainDB with `pip` diff --git a/docs/source/python-api-tutorial.md b/docs/source/python-api-tutorial.md new file mode 100644 index 00000000..84c64650 --- /dev/null +++ b/docs/source/python-api-tutorial.md @@ -0,0 +1,53 @@ +# Getting started with the HTTP API + +The preferred way to communicate with a Node in the BigchainDB Federation is via HTTP requests. +Each Node exposes a simple HTTP API that provides, right now, two endpoints, one to get information about a specific +transaction id, one to push transactions to the BigchainDB network. + +The endpoints are documented in [Apiary](http://docs.bigchaindb.apiary.io/). + + +## Usage example using the Python client + +```python +In [1]: from bigchaindb.client import temp_client +In [2]: c1 = temp_client() +In [3]: c2 = temp_client() +In [4]: tx1 = c1.create() +In [5]: tx1 +Out[5]: +{'assignee': '2Bi5NUv1UL7h3ZGs5AsE6Gr3oPQhE2vGsYCapNYrAU4pr', +'id': '26f21d8b5f9731cef631733b8cd1da05f87aa59eb2f939277a2fefeb774ae133', +'signature': '304402201b904f22e9f5a502070244b64822adf28...', +'transaction': {'current_owner': '2Bi5NUv1UL7h3ZGs5AsE6Gr3oPQhE2vGsYCapNYrAU4pr', + 'data': {'hash': 'efbde2c3aee204a69b7696d4b10ff31137fe78e3946306284f806e2dfc68b805', + 'payload': None}, + 'input': None, + 'new_owner': '247epGEcoX9m6yvR6sEZvYGb1XCpUUWtCNUVKgJGrFWCr', + 'operation': 'CREATE', + 'timestamp': '1456763521.824126'}} +In [7]: c1.transfer(c2.public_key, tx1['id']) +Out[7]: +{'assignee': '2Bi5NUv1UL7h3ZGs5AsE6Gr3oPQhE2vGsYCapNYrAU4pr', +'id': '34b62c9fdfd93f5907f35e2495239ae1cb62e9519ff64a8710f3f77a9f040857', +'signature': '3046022100b2b2432c20310dfcda6a2bab3c893b0cd17e70fe...', +'transaction': {'current_owner': '247epGEcoX9m6yvR6sEZvYGb1XCpUUWtCNUVKgJGrFWCr', + 'data': {'hash': 'efbde2c3aee204a69b7696d4b10ff31137fe78e3946306284f806e2dfc68b805', + 'payload': None}, + 'input': '26f21d8b5f9731cef631733b8cd1da05f87aa59eb2f939277a2fefeb774ae133', + 'new_owner': 'p5Ci1KJkPHvRBnxqyq36m8GXwkWSuhMiZSg8aB1ZrZgJ', + 'operation': 'TRANSFER', + 'timestamp': '1456763549.446138'}} +``` + + +# Roadmap + +The development of the API is still at the beginning and you can follow it on +[GitHub](https://github.com/bigchaindb/bigchaindb/issues?q=is%3Aissue+is%3Aopen+label%3Arest-api) + +There are several key features still missing like: + - validating the structure of the transaction + - returns the correct error codes if something goes wrong + - add an endpoint to query unspents for a given public key + diff --git a/setup.py b/setup.py index 26f111fa..083dc7a8 100644 --- a/setup.py +++ b/setup.py @@ -15,6 +15,7 @@ tests_require = [ 'pytest', 'pytest-cov', 'pytest-xdist', + 'pytest-flask', ] dev_require = [ @@ -26,6 +27,7 @@ docs_require = [ 'recommonmark>=0.4.0', 'Sphinx>=1.3.5', 'sphinxcontrib-napoleon>=0.4.4', + 'sphinx-rtd-theme>=0.1.9', ] setup( @@ -80,6 +82,8 @@ setup( 'logstats==0.2.1', 'base58==0.2.2', 'bitcoin==1.1.42', + 'flask==0.10.1', + 'requests==2.9', ], setup_requires=['pytest-runner'], tests_require=tests_require, diff --git a/tests/conftest.py b/tests/conftest.py index 2191f73b..3781b2b6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -7,13 +7,14 @@ Tasks: """ import os +import copy import pytest DB_NAME = 'bigchain_test_{}'.format(os.getpid()) -config = { +CONFIG = { 'database': { 'name': DB_NAME }, @@ -36,7 +37,7 @@ def restore_config(request, node_config): @pytest.fixture(scope='module') def node_config(): - return config + return copy.deepcopy(CONFIG) @pytest.fixture @@ -47,3 +48,11 @@ def user_private_key(): @pytest.fixture def user_public_key(): return USER_PUBLIC_KEY + + +@pytest.fixture +def b(request, node_config): + restore_config(request, node_config) + from bigchaindb import Bigchain + return Bigchain() + diff --git a/tests/db/conftest.py b/tests/db/conftest.py index b0545464..d79ee846 100644 --- a/tests/db/conftest.py +++ b/tests/db/conftest.py @@ -9,6 +9,7 @@ Tasks: import pytest import rethinkdb as r +import bigchaindb from bigchaindb import Bigchain from bigchaindb.db import get_conn @@ -80,6 +81,23 @@ def cleanup_tables(request, node_config): @pytest.fixture -def b(): - return Bigchain() +def inputs(user_public_key, amount=1, b=None): + # 1. create the genesis block + b = b or Bigchain() + try: + b.create_genesis_block() + except bigchaindb.core.GenesisBlockAlreadyExistsError: + pass + + # 2. create block with transactions for `USER` to spend + transactions = [] + for i in range(amount): + tx = b.create_transaction(b.me, user_public_key, None, 'CREATE') + tx_signed = b.sign_transaction(tx, b.me_private) + transactions.append(tx_signed) + b.write_transaction(tx_signed) + + block = b.create_block(transactions) + b.write_block(block, durability='hard') + return block diff --git a/tests/db/test_bigchain_api.py b/tests/db/test_bigchain_api.py index f956f1e2..1fed8800 100644 --- a/tests/db/test_bigchain_api.py +++ b/tests/db/test_bigchain_api.py @@ -6,38 +6,13 @@ import pytest import rethinkdb as r import bigchaindb +from bigchaindb import util from bigchaindb import exceptions -from bigchaindb import Bigchain -from bigchaindb.crypto import hash_data, PrivateKey, PublicKey, generate_key_pair +from bigchaindb.crypto import PrivateKey, PublicKey, generate_key_pair, hash_data from bigchaindb.voter import Voter from bigchaindb.block import Block -def create_inputs(user_public_key, amount=1, b=None): - # 1. create the genesis block - b = b or Bigchain() - try: - b.create_genesis_block() - except bigchaindb.core.GenesisBlockAlreadyExistsError: - pass - - # 2. create block with transactions for `USER` to spend - transactions = [] - for i in range(amount): - tx = b.create_transaction(b.me, user_public_key, None, 'CREATE') - tx_signed = b.sign_transaction(tx, b.me_private) - transactions.append(tx_signed) - b.write_transaction(tx_signed) - - block = b.create_block(transactions) - b.write_block(block, durability='hard') - return block - - -@pytest.fixture -def inputs(user_public_key): - return create_inputs(user_public_key) - @pytest.mark.skipif(reason='Some tests throw a ResourceWarning that might result in some weird ' 'exceptions while running the tests. The problem seems to *not* ' @@ -69,7 +44,7 @@ class TestBigchainApi(object): 'operation': 'd', 'timestamp': tx['transaction']['timestamp'], 'data': { - 'hash': hash_data(b.serialize(payload)), + 'hash': hash_data(util.serialize(payload)), 'payload': payload } } @@ -77,7 +52,7 @@ class TestBigchainApi(object): # assert tx_hash == tx_calculated_hash def test_transaction_signature(self, b): - sk, vk = b.generate_keys() + sk, vk = generate_key_pair() tx = b.create_transaction(vk, 'b', 'c', 'd') tx_signed = b.sign_transaction(tx, sk) @@ -86,7 +61,7 @@ class TestBigchainApi(object): def test_serializer(self, b): tx = b.create_transaction('a', 'b', 'c', 'd') - assert b.deserialize(b.serialize(tx)) == tx + assert util.deserialize(util.serialize(tx)) == tx @pytest.mark.usefixtures('inputs') def test_write_transaction(self, b, user_public_key, user_private_key): @@ -114,7 +89,7 @@ class TestBigchainApi(object): b.write_block(block, durability='hard') response = b.get_transaction(tx_signed["id"]) - assert b.serialize(tx_signed) == b.serialize(response) + assert util.serialize(tx_signed) == util.serialize(response) @pytest.mark.usefixtures('inputs') def test_assign_transaction_one_node(self, b, user_public_key, user_private_key): @@ -129,11 +104,11 @@ class TestBigchainApi(object): # 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_public_key, user_private_key): # create 5 federation nodes for _ in range(5): - b.federation_nodes.append(b.generate_keys()[1]) - create_inputs(user_public_key, amount=20, b=b) + b.federation_nodes.append(generate_key_pair()[1]) # test assignee for several transactions for _ in range(20): @@ -210,11 +185,11 @@ class TestBigchainApi(object): def test_create_new_block(self, b): new_block = b.create_block([]) - block_hash = hash_data(b.serialize(new_block['block'])) + block_hash = hash_data(util.serialize(new_block['block'])) assert new_block['block']['voters'] == [b.me] assert new_block['block']['node_pubkey'] == b.me - assert PublicKey(b.me).verify(b.serialize(new_block['block']), new_block['signature']) is True + assert PublicKey(b.me).verify(util.serialize(new_block['block']), new_block['signature']) is True assert new_block['id'] == block_hash assert new_block['votes'] == [] @@ -389,13 +364,13 @@ class TestBlockValidation(object): # create a block with invalid transactions block = { - 'timestamp': b.timestamp(), + 'timestamp': util.timestamp(), 'transactions': [tx_invalid], 'node_pubkey': b.me, 'voters': b.federation_nodes } - block_data = b.serialize(block) + block_data = util.serialize(block) block_hash = hash_data(block_data) block_signature = PrivateKey(b.me_private).sign(block_data) @@ -508,7 +483,7 @@ class TestBigchainVoter(object): assert vote['vote']['is_block_valid'] is True assert vote['vote']['invalid_reason'] is None assert vote['node_pubkey'] == b.me - assert PublicKey(b.me).verify(b.serialize(vote['vote']), vote['signature']) is True + assert PublicKey(b.me).verify(util.serialize(vote['vote']), vote['signature']) is True def test_invalid_block_voting(self, b, user_public_key): # create queue and voter @@ -549,7 +524,7 @@ class TestBigchainVoter(object): assert vote['vote']['is_block_valid'] is False assert vote['vote']['invalid_reason'] is None assert vote['node_pubkey'] == b.me - assert PublicKey(b.me).verify(b.serialize(vote['vote']), vote['signature']) is True + assert PublicKey(b.me).verify(util.serialize(vote['vote']), vote['signature']) is True def test_vote_creation_valid(self, b): # create valid block @@ -563,7 +538,7 @@ class TestBigchainVoter(object): assert vote['vote']['is_block_valid'] is True assert vote['vote']['invalid_reason'] is None assert vote['node_pubkey'] == b.me - assert PublicKey(b.me).verify(b.serialize(vote['vote']), vote['signature']) is True + assert PublicKey(b.me).verify(util.serialize(vote['vote']), vote['signature']) is True def test_vote_creation_invalid(self, b): # create valid block @@ -577,7 +552,7 @@ class TestBigchainVoter(object): assert vote['vote']['is_block_valid'] is False assert vote['vote']['invalid_reason'] is None assert vote['node_pubkey'] == b.me - assert PublicKey(b.me).verify(b.serialize(vote['vote']), vote['signature']) is True + assert PublicKey(b.me).verify(util.serialize(vote['vote']), vote['signature']) is True class TestBigchainBlock(object): diff --git a/tests/db/test_utils.py b/tests/db/test_utils.py index 287fdfcf..9e032b25 100644 --- a/tests/db/test_utils.py +++ b/tests/db/test_utils.py @@ -4,6 +4,7 @@ import pytest import rethinkdb as r import bigchaindb +from bigchaindb import util from bigchaindb.db import utils from .conftest import setup_database as _setup_database diff --git a/tests/db/test_voter.py b/tests/db/test_voter.py index 6800ce1c..d8146829 100644 --- a/tests/db/test_voter.py +++ b/tests/db/test_voter.py @@ -3,8 +3,10 @@ import time import rethinkdb as r import multiprocessing as mp +from bigchaindb import util + from bigchaindb.voter import Voter, BlockStream -from bigchaindb.crypto import PublicKey +from bigchaindb.crypto import PublicKey, generate_key_pair class TestBigchainVoter(object): @@ -43,7 +45,7 @@ class TestBigchainVoter(object): assert vote['vote']['is_block_valid'] is True assert vote['vote']['invalid_reason'] is None assert vote['node_pubkey'] == b.me - assert PublicKey(b.me).verify(b.serialize(vote['vote']), vote['signature']) is True + assert PublicKey(b.me).verify(util.serialize(vote['vote']), vote['signature']) is True def test_valid_block_voting_with_create_transaction(self, b): q_new_block = mp.Queue() @@ -51,7 +53,7 @@ class TestBigchainVoter(object): genesis = b.create_genesis_block() # create a `CREATE` transaction - test_user_priv, test_user_pub = b.generate_keys() + test_user_priv, test_user_pub = generate_key_pair() tx = b.create_transaction(b.me, test_user_pub, None, 'CREATE') tx_signed = b.sign_transaction(tx, b.me_private) assert b.is_valid_transaction(tx_signed) @@ -85,7 +87,7 @@ class TestBigchainVoter(object): assert vote['vote']['is_block_valid'] is True assert vote['vote']['invalid_reason'] is None assert vote['node_pubkey'] == b.me - assert PublicKey(b.me).verify(b.serialize(vote['vote']), vote['signature']) is True + assert PublicKey(b.me).verify(util.serialize(vote['vote']), vote['signature']) is True def test_valid_block_voting_with_transfer_transactions(self, b): q_new_block = mp.Queue() @@ -93,7 +95,7 @@ class TestBigchainVoter(object): b.create_genesis_block() # create a `CREATE` transaction - test_user_priv, test_user_pub = b.generate_keys() + test_user_priv, test_user_pub = generate_key_pair() tx = b.create_transaction(b.me, test_user_pub, None, 'CREATE') tx_signed = b.sign_transaction(tx, b.me_private) assert b.is_valid_transaction(tx_signed) @@ -122,7 +124,7 @@ class TestBigchainVoter(object): assert len(blocks[1]['votes']) == 1 # create a `TRANSFER` transaction - test_user2_priv, test_user2_pub = b.generate_keys() + test_user2_priv, test_user2_pub = generate_key_pair() tx2 = b.create_transaction(test_user_pub, test_user2_pub, tx['id'], 'TRANSFER') tx2_signed = b.sign_transaction(tx2, test_user_priv) assert b.is_valid_transaction(tx2_signed) @@ -156,7 +158,7 @@ class TestBigchainVoter(object): assert vote['vote']['is_block_valid'] is True assert vote['vote']['invalid_reason'] is None assert vote['node_pubkey'] == b.me - assert PublicKey(b.me).verify(b.serialize(vote['vote']), vote['signature']) is True + assert PublicKey(b.me).verify(util.serialize(vote['vote']), vote['signature']) is True def test_invalid_block_voting(self, b, user_public_key): # create queue and voter @@ -195,7 +197,7 @@ class TestBigchainVoter(object): assert vote['vote']['is_block_valid'] is False assert vote['vote']['invalid_reason'] is None assert vote['node_pubkey'] == b.me - assert PublicKey(b.me).verify(b.serialize(vote['vote']), vote['signature']) is True + assert PublicKey(b.me).verify(util.serialize(vote['vote']), vote['signature']) is True def test_vote_creation_valid(self, b): # create valid block @@ -209,7 +211,7 @@ class TestBigchainVoter(object): assert vote['vote']['is_block_valid'] is True assert vote['vote']['invalid_reason'] is None assert vote['node_pubkey'] == b.me - assert PublicKey(b.me).verify(b.serialize(vote['vote']), vote['signature']) is True + assert PublicKey(b.me).verify(util.serialize(vote['vote']), vote['signature']) is True def test_vote_creation_invalid(self, b): # create valid block @@ -223,7 +225,7 @@ class TestBigchainVoter(object): assert vote['vote']['is_block_valid'] is False assert vote['vote']['invalid_reason'] is None assert vote['node_pubkey'] == b.me - assert PublicKey(b.me).verify(b.serialize(vote['vote']), vote['signature']) is True + assert PublicKey(b.me).verify(util.serialize(vote['vote']), vote['signature']) is True def test_voter_considers_unvoted_blocks_when_single_node(self, b): # simulate a voter going donw in a single node environment @@ -299,7 +301,7 @@ class TestBlockStream(object): def test_if_federation_size_is_greater_than_one_ignore_past_blocks(self, b): for _ in range(5): - b.federation_nodes.append(b.generate_keys()[1]) + b.federation_nodes.append(generate_key_pair()[1]) new_blocks = mp.Queue() bs = BlockStream(new_blocks) block_1 = b.create_block([]) diff --git a/tests/test_client.py b/tests/test_client.py new file mode 100644 index 00000000..f5e15cad --- /dev/null +++ b/tests/test_client.py @@ -0,0 +1,59 @@ +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) + + +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 + + +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. + # `current_owner` 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']['current_owner'] == client.public_key + assert tx['transaction']['new_owner'] == client.public_key + assert tx['transaction']['input'] == None + + assert util.verify_signature(tx) + + +def test_client_can_transfer_assets(mock_requests_post, client): + from bigchaindb import util + + tx = client.transfer('a', 123) + + assert tx['transaction']['current_owner'] == client.public_key + assert tx['transaction']['new_owner'] == 'a' + assert tx['transaction']['input'] == 123 + + assert util.verify_signature(tx) + diff --git a/tests/test_commands.py b/tests/test_commands.py index 57ebdbe9..6e731c13 100644 --- a/tests/test_commands.py +++ b/tests/test_commands.py @@ -87,7 +87,7 @@ def test_bigchain_run_start_assume_yes_create_default_config(monkeypatch, mock_p value['return'] = newconfig monkeypatch.setattr(config_utils, 'write_config', mock_write_config) - monkeypatch.setattr(config_utils, 'file_config', lambda x: config_utils.dict_config(value['return'])) + monkeypatch.setattr(config_utils, 'file_config', lambda x: config_utils.dict_config(expected_config)) monkeypatch.setattr('os.path.exists', lambda path: False) args = Namespace(config=None, yes=True) diff --git a/tests/test_core.py b/tests/test_core.py index 6374b8be..9857b37f 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -1,10 +1,8 @@ -import copy import pytest @pytest.fixture -def config(request): - import bigchaindb +def config(request, monkeypatch): config = { 'database': { 'host': 'host', @@ -18,13 +16,10 @@ def config(request): 'keyring': [], 'CONFIGURED': True, } - bigchaindb.config.update(config) - def fin(): - bigchaindb.config = bigchaindb._config - bigchaindb._config = copy.deepcopy(bigchaindb._config) - request.addfinalizer(fin) - return bigchaindb.config + monkeypatch.setattr('bigchaindb.config', config) + + return config def test_bigchain_class_default_initialization(config): diff --git a/tests/test_util.py b/tests/test_util.py new file mode 100644 index 00000000..f4708b59 --- /dev/null +++ b/tests/test_util.py @@ -0,0 +1,12 @@ +from bigchaindb import util + + +def test_transform_create(b, user_private_key, user_public_key): + tx = util.create_tx(user_public_key, user_public_key, None, 'CREATE') + tx = util.transform_create(tx) + tx = util.sign_tx(tx, b.me_private) + + assert tx['transaction']['current_owner'] == b.me + assert tx['transaction']['new_owner'] == user_public_key + assert util.verify_signature(tx) + diff --git a/tests/utils/test_config_utils.py b/tests/utils/test_config_utils.py index 57539246..2cf57242 100644 --- a/tests/utils/test_config_utils.py +++ b/tests/utils/test_config_utils.py @@ -3,14 +3,15 @@ import copy import pytest import bigchaindb +from bigchaindb import exceptions ORIGINAL_CONFIG = copy.deepcopy(bigchaindb._config) @pytest.fixture(scope='function', autouse=True) -def clean_config(): - bigchaindb.config = copy.deepcopy(ORIGINAL_CONFIG) +def clean_config(monkeypatch): + monkeypatch.setattr('bigchaindb.config', copy.deepcopy(ORIGINAL_CONFIG)) def test_bigchain_instance_is_initialized_when_conf_provided(): @@ -34,5 +35,5 @@ def test_bigchain_instance_raises_when_not_configured(monkeypatch): # from existing configurations monkeypatch.setattr(config_utils, 'autoconfigure', lambda: 0) - with pytest.raises(bigchaindb.core.KeypairNotFoundException): + with pytest.raises(exceptions.KeypairNotFoundException): bigchaindb.Bigchain() diff --git a/tests/web/__init__.py b/tests/web/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/web/conftest.py b/tests/web/conftest.py new file mode 100644 index 00000000..099f2fd3 --- /dev/null +++ b/tests/web/conftest.py @@ -0,0 +1,35 @@ +import pytest +from ..db import conftest + + +@pytest.fixture(autouse=True) +def restore_config(request, node_config): + from bigchaindb import config_utils + config_utils.dict_config(node_config) + + +@pytest.fixture(scope='module', autouse=True) +def setup_database(request, node_config): + conftest.setup_database(request, node_config) + + +@pytest.fixture(scope='function', autouse=True) +def cleanup_tables(request, node_config): + conftest.cleanup_tables(request, node_config) + + +@pytest.fixture +def app(request, node_config): + # XXX: For whatever reason this fixture runs before `restore_config`, + # so we need to manually call it. + restore_config(request, node_config) + + from bigchaindb.web import server + app = server.create_app(debug=True) + return app + + +@pytest.fixture +def inputs(user_public_key): + conftest.inputs(user_public_key) + diff --git a/tests/web/test_basic_views.py b/tests/web/test_basic_views.py new file mode 100644 index 00000000..04a1c292 --- /dev/null +++ b/tests/web/test_basic_views.py @@ -0,0 +1,42 @@ +import json + +import pytest +from bigchaindb import crypto +from bigchaindb import util + + +TX_ENDPOINT = '/api/v1/transactions/' + + +@pytest.mark.usefixtures('inputs') +def test_get_transaction_endpoint(b, client, user_public_key): + input_tx = b.get_owned_ids(user_public_key).pop() + tx = b.get_transaction(input_tx) + res = client.get(TX_ENDPOINT + input_tx) + assert tx == res.json + + +def test_post_create_transaction_endpoint(b, client): + keypair = crypto.generate_key_pair() + + tx = util.create_and_sign_tx(keypair[0], keypair[1], keypair[1], None, 'CREATE') + + res = client.post(TX_ENDPOINT, data=json.dumps(tx)) + assert res.json['transaction']['current_owner'] == b.me + assert res.json['transaction']['new_owner'] == keypair[1] + + +def test_post_transfer_transaction_endpoint(b, client): + from_keypair = crypto.generate_key_pair() + to_keypair = crypto.generate_key_pair() + + tx = util.create_and_sign_tx(from_keypair[0], from_keypair[1], from_keypair[1], None, 'CREATE') + res = client.post(TX_ENDPOINT, data=json.dumps(tx)) + tx_id = res.json['id'] + + transfer = util.create_and_sign_tx(from_keypair[0], from_keypair[1], to_keypair[1], tx_id) + res = client.post(TX_ENDPOINT, data=json.dumps(transfer)) + + assert res.json['transaction']['current_owner'] == from_keypair[1] + assert res.json['transaction']['new_owner'] == to_keypair[1] +