Merge remote-tracking branch 'upstream/develop' into

modular-consensus-rules
Resolves conflicts from module reorg in PR #102

Conflicts:
	bigchaindb/__init__.py
	bigchaindb/core.py
This commit is contained in:
Matt Smith 2016-03-04 18:07:23 -08:00
commit 9644df07f7
33 changed files with 751 additions and 267 deletions

View File

@ -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?

View File

@ -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/)

View File

@ -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)

View File

@ -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

97
bigchaindb/client.py Normal file
View File

@ -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')

View File

@ -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()

View File

@ -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()

View File

@ -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()

View File

@ -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"""

View File

@ -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()

View File

@ -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

View File

21
bigchaindb/web/server.py Normal file
View File

@ -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

68
bigchaindb/web/views.py Normal file
View File

@ -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/<tx_id>')
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)

View File

@ -1,3 +1,4 @@
Sphinx==1.3.5
sphinxcontrib-napoleon==0.4.4
sphinx-rtd-theme>=0.1.9
recommonmark

View File

@ -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
# "<project> v<release> documentation".

View File

@ -16,6 +16,7 @@ Table of Contents
installing
getting-started
bigchaindb-cli
python-api-tutorial
admin
cryptography
models

View File

@ -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`

View File

@ -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

View File

@ -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,

View File

@ -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()

View File

@ -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

View File

@ -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):

View File

@ -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

View File

@ -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([])

59
tests/test_client.py Normal file
View File

@ -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)

View File

@ -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)

View File

@ -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):

12
tests/test_util.py Normal file
View File

@ -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)

View File

@ -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()

0
tests/web/__init__.py Normal file
View File

35
tests/web/conftest.py Normal file
View File

@ -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)

View File

@ -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]