mirror of
https://github.com/bigchaindb/bigchaindb.git
synced 2024-10-13 13:34:05 +00:00
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:
commit
9644df07f7
@ -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?
|
||||
|
||||
|
@ -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/)
|
||||
|
||||
|
22
ROADMAP.md
22
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)
|
||||
|
@ -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
97
bigchaindb/client.py
Normal 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')
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
|
||||
|
||||
|
@ -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"""
|
||||
|
||||
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
0
bigchaindb/web/__init__.py
Normal file
0
bigchaindb/web/__init__.py
Normal file
21
bigchaindb/web/server.py
Normal file
21
bigchaindb/web/server.py
Normal 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
68
bigchaindb/web/views.py
Normal 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)
|
||||
|
@ -1,3 +1,4 @@
|
||||
Sphinx==1.3.5
|
||||
sphinxcontrib-napoleon==0.4.4
|
||||
sphinx-rtd-theme>=0.1.9
|
||||
recommonmark
|
||||
|
@ -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".
|
||||
|
@ -16,6 +16,7 @@ Table of Contents
|
||||
installing
|
||||
getting-started
|
||||
bigchaindb-cli
|
||||
python-api-tutorial
|
||||
admin
|
||||
cryptography
|
||||
models
|
||||
|
@ -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`
|
||||
|
53
docs/source/python-api-tutorial.md
Normal file
53
docs/source/python-api-tutorial.md
Normal 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
|
||||
|
4
setup.py
4
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,
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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):
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
59
tests/test_client.py
Normal 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)
|
||||
|
@ -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)
|
||||
|
@ -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
12
tests/test_util.py
Normal 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)
|
||||
|
@ -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
0
tests/web/__init__.py
Normal file
35
tests/web/conftest.py
Normal file
35
tests/web/conftest.py
Normal 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)
|
||||
|
42
tests/web/test_basic_views.py
Normal file
42
tests/web/test_basic_views.py
Normal 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]
|
||||
|
Loading…
x
Reference in New Issue
Block a user