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."
|
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?
|
### 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
|
* [Licenses](LICENSES.md) - open source & open content
|
||||||
* [Imprint](https://www.bigchaindb.com/imprint/)
|
* [Imprint](https://www.bigchaindb.com/imprint/)
|
||||||
* [Contact Us](https://www.bigchaindb.com/contact/)
|
* [Contact Us](https://www.bigchaindb.com/contact/)
|
||||||
|
|
||||||
|
22
ROADMAP.md
22
ROADMAP.md
@ -2,20 +2,28 @@
|
|||||||
|
|
||||||
## BigchainDB Protocols
|
## BigchainDB Protocols
|
||||||
* Validation of other nodes
|
* Validation of other nodes
|
||||||
* Byzantine fault tolerance
|
* Fault tolerance
|
||||||
* Permissions framework
|
* 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
|
* Protocol audits including security audits
|
||||||
|
|
||||||
## Implementation/Code
|
## Implementation/Code
|
||||||
* Node validation framework (inspect and agree or not with what the other nodes are doing)
|
* Node validation framework (inspect and agree or not with what the other nodes are doing)
|
||||||
* Federation management and monitoring/dashboard
|
* Federation management tools
|
||||||
* Packaging, dockerization, AWS image, etc. (i.e. easy deployment options)
|
* More tools for benchmarking a cluster
|
||||||
* Drivers/SDKs for common client-side languages (e.g. Python, Ruby, JavaScript, Java)
|
* 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)
|
* ORM to better-decouple BigchainDB from its data store (will make it easy to try other databases)
|
||||||
* Code audits including security audits
|
* Code audits including security audits
|
||||||
|
|
||||||
## Other/Future
|
## Other/Future
|
||||||
* Multisig
|
* Byzantine fault tolerance
|
||||||
* Better support for smart contract frameworks
|
* 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),
|
'port': e('BIGCHAIN_STATSD_PORT', default=8125),
|
||||||
'rate': e('BIGCHAIN_STATSD_SAMPLERATE', default=0.01)
|
'rate': e('BIGCHAIN_STATSD_SAMPLERATE', default=0.01)
|
||||||
},
|
},
|
||||||
|
'api_endpoint': 'http://localhost:8008/api/v1',
|
||||||
'consensus_plugins': [
|
'consensus_plugins': [
|
||||||
'base'
|
'base'
|
||||||
],
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
# We need to maintain a backup copy of the original config dict in case
|
# We need to maintain a backup copy of the original config dict in case
|
||||||
@ -50,4 +51,3 @@ config = {
|
|||||||
# for more info.
|
# for more info.
|
||||||
_config = copy.deepcopy(config)
|
_config = copy.deepcopy(config)
|
||||||
from bigchaindb.core import Bigchain # noqa
|
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
|
import bigchaindb.exceptions as exceptions
|
||||||
|
from bigchaindb import util
|
||||||
from bigchaindb.crypto import hash_data
|
from bigchaindb.crypto import hash_data
|
||||||
from bigchaindb.consensus import AbstractConsensusRules
|
from bigchaindb.consensus import AbstractConsensusRules
|
||||||
|
|
||||||
@ -72,7 +73,7 @@ class ConsensusRules(AbstractConsensusRules):
|
|||||||
transaction['transaction']['input']))
|
transaction['transaction']['input']))
|
||||||
|
|
||||||
# Check hash of the transaction
|
# Check hash of the transaction
|
||||||
calculated_hash = hash_data(bigchain.serialize(
|
calculated_hash = hash_data(util.serialize(
|
||||||
transaction['transaction']))
|
transaction['transaction']))
|
||||||
if calculated_hash != transaction['id']:
|
if calculated_hash != transaction['id']:
|
||||||
raise exceptions.InvalidHash()
|
raise exceptions.InvalidHash()
|
||||||
@ -101,7 +102,7 @@ class ConsensusRules(AbstractConsensusRules):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
# Check if current hash is correct
|
# 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']:
|
if calculated_hash != block['id']:
|
||||||
raise exceptions.InvalidHash()
|
raise exceptions.InvalidHash()
|
||||||
|
|
||||||
|
@ -1,17 +1,17 @@
|
|||||||
import rethinkdb as r
|
import rethinkdb as r
|
||||||
import time
|
|
||||||
import random
|
import random
|
||||||
import json
|
import json
|
||||||
import rapidjson
|
import rapidjson
|
||||||
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
import bigchaindb
|
import bigchaindb
|
||||||
|
from bigchaindb import util
|
||||||
from bigchaindb import config_utils
|
from bigchaindb import config_utils
|
||||||
from bigchaindb import exceptions
|
from bigchaindb import exceptions
|
||||||
from bigchaindb.crypto import hash_data, PublicKey, PrivateKey, generate_key_pair
|
from bigchaindb import crypto
|
||||||
from bigchaindb.monitor import Monitor
|
from bigchaindb.monitor import Monitor
|
||||||
|
|
||||||
|
|
||||||
monitor = Monitor()
|
monitor = Monitor()
|
||||||
|
|
||||||
|
|
||||||
@ -19,10 +19,6 @@ class GenesisBlockAlreadyExistsError(Exception):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class KeypairNotFoundException(Exception):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class Bigchain(object):
|
class Bigchain(object):
|
||||||
"""Bigchain API
|
"""Bigchain API
|
||||||
|
|
||||||
@ -48,8 +44,8 @@ class Bigchain(object):
|
|||||||
public_key (str): the base58 encoded public key for the ECDSA secp256k1 curve.
|
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.
|
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.
|
keyring (list[str]): list of base58 encoded public keys of the federation nodes.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
config_utils.autoconfigure()
|
config_utils.autoconfigure()
|
||||||
self.host = host or bigchaindb.config['database']['host']
|
self.host = host or bigchaindb.config['database']['host']
|
||||||
self.port = port or bigchaindb.config['database']['port']
|
self.port = port or bigchaindb.config['database']['port']
|
||||||
@ -60,7 +56,7 @@ class Bigchain(object):
|
|||||||
self.consensus_plugins = config_utils.get_plugins(consensus_plugins)
|
self.consensus_plugins = config_utils.get_plugins(consensus_plugins)
|
||||||
|
|
||||||
if not self.me or not self.me_private:
|
if not self.me or not self.me_private:
|
||||||
raise KeypairNotFoundException()
|
raise exceptions.KeypairNotFoundException()
|
||||||
|
|
||||||
self._conn = None
|
self._conn = None
|
||||||
|
|
||||||
@ -77,101 +73,25 @@ class Bigchain(object):
|
|||||||
def create_transaction(self, current_owner, new_owner, tx_input, operation, payload=None):
|
def create_transaction(self, current_owner, new_owner, tx_input, operation, payload=None):
|
||||||
"""Create a new transaction
|
"""Create a new transaction
|
||||||
|
|
||||||
A transaction in the bigchain is a transfer of a digital asset between two entities represented
|
Refer to the documentation of ``bigchaindb.util.create_tx``
|
||||||
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(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))
|
return util.create_tx(current_owner, new_owner, tx_input, operation, 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
|
|
||||||
|
|
||||||
def sign_transaction(self, transaction, private_key):
|
def sign_transaction(self, transaction, private_key):
|
||||||
"""Sign a transaction
|
"""Sign a transaction
|
||||||
|
|
||||||
A transaction signed with the `current_owner` corresponding private key.
|
Refer to the documentation of ``bigchaindb.util.sign_tx``
|
||||||
|
|
||||||
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(self.serialize(transaction))
|
return util.sign_tx(transaction, private_key)
|
||||||
signed_transaction = transaction.copy()
|
|
||||||
signed_transaction.update({'signature': signature})
|
|
||||||
return signed_transaction
|
|
||||||
|
|
||||||
def verify_signature(self, signed_transaction):
|
def verify_signature(self, signed_transaction):
|
||||||
"""Verify the signature of a 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.
|
|
||||||
|
|
||||||
|
Refer to the documentation of ``bigchaindb.crypto.verify_signature``
|
||||||
"""
|
"""
|
||||||
|
|
||||||
data = signed_transaction.copy()
|
data = signed_transaction.copy()
|
||||||
|
|
||||||
# if assignee field in the transaction, remove it
|
# if assignee field in the transaction, remove it
|
||||||
@ -180,11 +100,11 @@ class Bigchain(object):
|
|||||||
|
|
||||||
signature = data.pop('signature')
|
signature = data.pop('signature')
|
||||||
public_key_base58 = signed_transaction['transaction']['current_owner']
|
public_key_base58 = signed_transaction['transaction']['current_owner']
|
||||||
public_key = PublicKey(public_key_base58)
|
public_key = crypto.PublicKey(public_key_base58)
|
||||||
return public_key.verify(self.serialize(data), signature)
|
return public_key.verify(util.serialize(data), signature)
|
||||||
|
|
||||||
@monitor.timer('write_transaction', rate=bigchaindb.config['statsd']['rate'])
|
@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.
|
"""Write the transaction to bigchain.
|
||||||
|
|
||||||
When first writing a transaction to the bigchain the transaction will be kept in a backlog until
|
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})
|
signed_transaction.update({'assignee': assignee})
|
||||||
|
|
||||||
# write to the backlog
|
# 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
|
return response
|
||||||
|
|
||||||
# TODO: the same `txid` can be in two different blocks
|
# 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.
|
A dict with the transaction details if the transaction was found.
|
||||||
|
|
||||||
If no transaction with that `txid` was found it returns `None`
|
If no transaction with that `txid` was found it returns `None`
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
response = r.table('bigchain').concat_map(lambda doc: doc['block']['transactions'])\
|
response = r.table('bigchain').concat_map(lambda doc: doc['block']['transactions'])\
|
||||||
.filter(lambda transaction: transaction['id'] == txid).run(self.conn)
|
.filter(lambda transaction: transaction['id'] == txid).run(self.conn)
|
||||||
|
|
||||||
@ -257,8 +177,8 @@ class Bigchain(object):
|
|||||||
Returns:
|
Returns:
|
||||||
A list of transactions containing that payload. If no transaction exists with that payload it
|
A list of transactions containing that payload. If no transaction exists with that payload it
|
||||||
returns `None`
|
returns `None`
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
cursor = r.table('bigchain')\
|
cursor = r.table('bigchain')\
|
||||||
.get_all(payload_hash, index='payload_hash')\
|
.get_all(payload_hash, index='payload_hash')\
|
||||||
.run(self.conn)
|
.run(self.conn)
|
||||||
@ -277,7 +197,6 @@ class Bigchain(object):
|
|||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The transaction that used the `txid` as an input if it exists else it returns `None`
|
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 an input was already spent
|
||||||
# checks if the bigchain has any transaction with input `transaction_id`
|
# checks if the bigchain has any transaction with input `transaction_id`
|
||||||
@ -303,8 +222,8 @@ class Bigchain(object):
|
|||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
list: list of `txids` currently owned by `owner`
|
list: list of `txids` currently owned by `owner`
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
response = r.table('bigchain')\
|
response = r.table('bigchain')\
|
||||||
.concat_map(lambda doc: doc['block']['transactions'])\
|
.concat_map(lambda doc: doc['block']['transactions'])\
|
||||||
.filter({'transaction': {'new_owner': owner}})\
|
.filter({'transaction': {'new_owner': owner}})\
|
||||||
@ -347,8 +266,8 @@ class Bigchain(object):
|
|||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: `True` if the transaction is valid, `False` otherwise
|
bool: `True` if the transaction is valid, `False` otherwise
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.validate_transaction(transaction)
|
self.validate_transaction(transaction)
|
||||||
return transaction
|
return transaction
|
||||||
@ -368,20 +287,20 @@ class Bigchain(object):
|
|||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
dict: created block.
|
dict: created block.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Create the new block
|
# Create the new block
|
||||||
block = {
|
block = {
|
||||||
'timestamp': self.timestamp(),
|
'timestamp': util.timestamp(),
|
||||||
'transactions': validated_transactions,
|
'transactions': validated_transactions,
|
||||||
'node_pubkey': self.me,
|
'node_pubkey': self.me,
|
||||||
'voters': self.federation_nodes + [self.me]
|
'voters': self.federation_nodes + [self.me]
|
||||||
}
|
}
|
||||||
|
|
||||||
# Calculate the hash of the new block
|
# Calculate the hash of the new block
|
||||||
block_data = self.serialize(block)
|
block_data = util.serialize(block)
|
||||||
block_hash = hash_data(block_data)
|
block_hash = crypto.hash_data(block_data)
|
||||||
block_signature = PrivateKey(self.me_private).sign(block_data)
|
block_signature = crypto.PrivateKey(self.me_private).sign(block_data)
|
||||||
|
|
||||||
block = {
|
block = {
|
||||||
'id': block_hash,
|
'id': block_hash,
|
||||||
@ -403,7 +322,6 @@ class Bigchain(object):
|
|||||||
Returns:
|
Returns:
|
||||||
The block if the block is valid else it raises and exception
|
The block if the block is valid else it raises and exception
|
||||||
describing the reason why the block is invalid.
|
describing the reason why the block is invalid.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# First run all of the plugin block validation logic
|
# First run all of the plugin block validation logic
|
||||||
@ -430,8 +348,8 @@ class Bigchain(object):
|
|||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
bool: `True` if the block is valid, `False` otherwise.
|
bool: `True` if the block is valid, `False` otherwise.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.validate_block(block)
|
self.validate_block(block)
|
||||||
return True
|
return True
|
||||||
@ -444,8 +362,8 @@ class Bigchain(object):
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
block (dict): block to write to bigchain.
|
block (dict): block to write to bigchain.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
block_serialized = rapidjson.dumps(block)
|
block_serialized = rapidjson.dumps(block)
|
||||||
r.table('bigchain').insert(r.json(block_serialized), durability=durability).run(self.conn)
|
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.
|
previous_block_id (str): The id of the previous block.
|
||||||
decision (bool): Whether the block is valid or invalid.
|
decision (bool): Whether the block is valid or invalid.
|
||||||
invalid_reason (Optional[str]): Reason the block is invalid
|
invalid_reason (Optional[str]): Reason the block is invalid
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
vote = {
|
vote = {
|
||||||
'voting_for_block': block['id'],
|
'voting_for_block': block['id'],
|
||||||
'previous_block': previous_block_id,
|
'previous_block': previous_block_id,
|
||||||
'is_block_valid': decision,
|
'is_block_valid': decision,
|
||||||
'invalid_reason': invalid_reason,
|
'invalid_reason': invalid_reason,
|
||||||
'timestamp': self.timestamp()
|
'timestamp': util.timestamp()
|
||||||
}
|
}
|
||||||
|
|
||||||
vote_data = self.serialize(vote)
|
vote_data = util.serialize(vote)
|
||||||
signature = PrivateKey(self.me_private).sign(vote_data)
|
signature = crypto.PrivateKey(self.me_private).sign(vote_data)
|
||||||
|
|
||||||
vote_signed = {
|
vote_signed = {
|
||||||
'node_pubkey': self.me,
|
'node_pubkey': self.me,
|
||||||
@ -514,9 +432,8 @@ class Bigchain(object):
|
|||||||
return vote_signed
|
return vote_signed
|
||||||
|
|
||||||
def write_vote(self, block, vote, block_number):
|
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)}
|
update = {'votes': r.row['votes'].append(vote)}
|
||||||
|
|
||||||
# We need to *not* override the existing block_number, if any
|
# We need to *not* override the existing block_number, if any
|
||||||
@ -530,9 +447,8 @@ class Bigchain(object):
|
|||||||
.run(self.conn)
|
.run(self.conn)
|
||||||
|
|
||||||
def get_last_voted_block(self):
|
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
|
# query bigchain for all blocks this node is a voter but didn't voted on
|
||||||
last_voted = r.table('bigchain')\
|
last_voted = r.table('bigchain')\
|
||||||
.filter(r.row['block']['voters'].contains(self.me))\
|
.filter(r.row['block']['voters'].contains(self.me))\
|
||||||
@ -551,9 +467,7 @@ class Bigchain(object):
|
|||||||
return last_voted[0]
|
return last_voted[0]
|
||||||
|
|
||||||
def get_unvoted_blocks(self):
|
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')\
|
unvoted = r.table('bigchain')\
|
||||||
.filter(lambda doc: doc['votes'].contains(lambda vote: vote['node_pubkey'] == self.me).not_())\
|
.filter(lambda doc: doc['votes'].contains(lambda vote: vote['node_pubkey'] == self.me).not_())\
|
||||||
@ -564,60 +478,3 @@ class Bigchain(object):
|
|||||||
unvoted.pop(0)
|
unvoted.pop(0)
|
||||||
|
|
||||||
return unvoted
|
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
|
# Separate all crypto code so that we can easily test several implementations
|
||||||
|
|
||||||
import hashlib
|
|
||||||
import sha3
|
|
||||||
import binascii
|
import binascii
|
||||||
import base58
|
import base58
|
||||||
|
|
||||||
|
import sha3
|
||||||
import bitcoin
|
import bitcoin
|
||||||
|
|
||||||
from cryptography.hazmat.backends import default_backend
|
from cryptography.hazmat.backends import default_backend
|
||||||
@ -148,4 +148,8 @@ def generate_key_pair():
|
|||||||
|
|
||||||
|
|
||||||
def hash_data(data):
|
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):
|
class DatabaseDoesNotExist(Exception):
|
||||||
"""Raised when trying to delete the database but the db is not there"""
|
"""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 import Bigchain
|
||||||
from bigchaindb.voter import Voter
|
from bigchaindb.voter import Voter
|
||||||
from bigchaindb.block import Block
|
from bigchaindb.block import Block
|
||||||
|
from bigchaindb.web import server
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@ -80,3 +81,9 @@ class Processes(object):
|
|||||||
|
|
||||||
logger.info('starting voter')
|
logger.info('starting voter')
|
||||||
p_voter.start()
|
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
|
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):
|
class ProcessGroup(object):
|
||||||
@ -22,3 +30,191 @@ class ProcessGroup(object):
|
|||||||
proc.start()
|
proc.start()
|
||||||
self.processes.append(proc)
|
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
|
Sphinx==1.3.5
|
||||||
sphinxcontrib-napoleon==0.4.4
|
sphinxcontrib-napoleon==0.4.4
|
||||||
|
sphinx-rtd-theme>=0.1.9
|
||||||
recommonmark
|
recommonmark
|
||||||
|
@ -28,6 +28,9 @@ from recommonmark.parser import CommonMarkParser
|
|||||||
# Add any Sphinx extension module names here, as strings. They can be
|
# Add any Sphinx extension module names here, as strings. They can be
|
||||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||||
# ones.
|
# ones.
|
||||||
|
import sphinx_rtd_theme
|
||||||
|
|
||||||
|
|
||||||
extensions = [
|
extensions = [
|
||||||
'sphinx.ext.autodoc',
|
'sphinx.ext.autodoc',
|
||||||
'sphinx.ext.intersphinx',
|
'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
|
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||||
# a list of builtin themes.
|
# 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
|
# 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
|
# further. For a list of options available for each theme, see the
|
||||||
@ -127,7 +131,7 @@ html_theme = 'alabaster'
|
|||||||
#html_theme_options = {}
|
#html_theme_options = {}
|
||||||
|
|
||||||
# Add any paths that contain custom themes here, relative to this directory.
|
# 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
|
# The name for this set of Sphinx documents. If None, it defaults to
|
||||||
# "<project> v<release> documentation".
|
# "<project> v<release> documentation".
|
||||||
|
@ -16,6 +16,7 @@ Table of Contents
|
|||||||
installing
|
installing
|
||||||
getting-started
|
getting-started
|
||||||
bigchaindb-cli
|
bigchaindb-cli
|
||||||
|
python-api-tutorial
|
||||||
admin
|
admin
|
||||||
cryptography
|
cryptography
|
||||||
models
|
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/).
|
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
|
```text
|
||||||
$ sudo apt-get update
|
$ sudo apt-get update
|
||||||
$ sudo apt-get install libffi-dev g++ libssl-dev python3-dev
|
$ 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.
|
With OS-level dependencies installed, you can install BigchainDB with `pip` or from source.
|
||||||
|
|
||||||
### How to Install BigchainDB with `pip`
|
### 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',
|
||||||
'pytest-cov',
|
'pytest-cov',
|
||||||
'pytest-xdist',
|
'pytest-xdist',
|
||||||
|
'pytest-flask',
|
||||||
]
|
]
|
||||||
|
|
||||||
dev_require = [
|
dev_require = [
|
||||||
@ -26,6 +27,7 @@ docs_require = [
|
|||||||
'recommonmark>=0.4.0',
|
'recommonmark>=0.4.0',
|
||||||
'Sphinx>=1.3.5',
|
'Sphinx>=1.3.5',
|
||||||
'sphinxcontrib-napoleon>=0.4.4',
|
'sphinxcontrib-napoleon>=0.4.4',
|
||||||
|
'sphinx-rtd-theme>=0.1.9',
|
||||||
]
|
]
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
@ -80,6 +82,8 @@ setup(
|
|||||||
'logstats==0.2.1',
|
'logstats==0.2.1',
|
||||||
'base58==0.2.2',
|
'base58==0.2.2',
|
||||||
'bitcoin==1.1.42',
|
'bitcoin==1.1.42',
|
||||||
|
'flask==0.10.1',
|
||||||
|
'requests==2.9',
|
||||||
],
|
],
|
||||||
setup_requires=['pytest-runner'],
|
setup_requires=['pytest-runner'],
|
||||||
tests_require=tests_require,
|
tests_require=tests_require,
|
||||||
|
@ -7,13 +7,14 @@ Tasks:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import copy
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
DB_NAME = 'bigchain_test_{}'.format(os.getpid())
|
DB_NAME = 'bigchain_test_{}'.format(os.getpid())
|
||||||
|
|
||||||
config = {
|
CONFIG = {
|
||||||
'database': {
|
'database': {
|
||||||
'name': DB_NAME
|
'name': DB_NAME
|
||||||
},
|
},
|
||||||
@ -36,7 +37,7 @@ def restore_config(request, node_config):
|
|||||||
|
|
||||||
@pytest.fixture(scope='module')
|
@pytest.fixture(scope='module')
|
||||||
def node_config():
|
def node_config():
|
||||||
return config
|
return copy.deepcopy(CONFIG)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
@ -47,3 +48,11 @@ def user_private_key():
|
|||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def user_public_key():
|
def user_public_key():
|
||||||
return 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 pytest
|
||||||
import rethinkdb as r
|
import rethinkdb as r
|
||||||
|
|
||||||
|
import bigchaindb
|
||||||
from bigchaindb import Bigchain
|
from bigchaindb import Bigchain
|
||||||
from bigchaindb.db import get_conn
|
from bigchaindb.db import get_conn
|
||||||
|
|
||||||
@ -80,6 +81,23 @@ def cleanup_tables(request, node_config):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def b():
|
def inputs(user_public_key, amount=1, b=None):
|
||||||
return Bigchain()
|
# 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 rethinkdb as r
|
||||||
|
|
||||||
import bigchaindb
|
import bigchaindb
|
||||||
|
from bigchaindb import util
|
||||||
from bigchaindb import exceptions
|
from bigchaindb import exceptions
|
||||||
from bigchaindb import Bigchain
|
from bigchaindb.crypto import PrivateKey, PublicKey, generate_key_pair, hash_data
|
||||||
from bigchaindb.crypto import hash_data, PrivateKey, PublicKey, generate_key_pair
|
|
||||||
from bigchaindb.voter import Voter
|
from bigchaindb.voter import Voter
|
||||||
from bigchaindb.block import Block
|
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 '
|
@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* '
|
'exceptions while running the tests. The problem seems to *not* '
|
||||||
@ -69,7 +44,7 @@ class TestBigchainApi(object):
|
|||||||
'operation': 'd',
|
'operation': 'd',
|
||||||
'timestamp': tx['transaction']['timestamp'],
|
'timestamp': tx['transaction']['timestamp'],
|
||||||
'data': {
|
'data': {
|
||||||
'hash': hash_data(b.serialize(payload)),
|
'hash': hash_data(util.serialize(payload)),
|
||||||
'payload': payload
|
'payload': payload
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -77,7 +52,7 @@ class TestBigchainApi(object):
|
|||||||
# assert tx_hash == tx_calculated_hash
|
# assert tx_hash == tx_calculated_hash
|
||||||
|
|
||||||
def test_transaction_signature(self, b):
|
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 = b.create_transaction(vk, 'b', 'c', 'd')
|
||||||
tx_signed = b.sign_transaction(tx, sk)
|
tx_signed = b.sign_transaction(tx, sk)
|
||||||
|
|
||||||
@ -86,7 +61,7 @@ class TestBigchainApi(object):
|
|||||||
|
|
||||||
def test_serializer(self, b):
|
def test_serializer(self, b):
|
||||||
tx = b.create_transaction('a', 'b', 'c', 'd')
|
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')
|
@pytest.mark.usefixtures('inputs')
|
||||||
def test_write_transaction(self, b, user_public_key, user_private_key):
|
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')
|
b.write_block(block, durability='hard')
|
||||||
|
|
||||||
response = b.get_transaction(tx_signed["id"])
|
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')
|
@pytest.mark.usefixtures('inputs')
|
||||||
def test_assign_transaction_one_node(self, b, user_public_key, user_private_key):
|
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
|
# check if the assignee is the current node
|
||||||
assert response['assignee'] == b.me
|
assert response['assignee'] == b.me
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures('inputs')
|
||||||
def test_assign_transaction_multiple_nodes(self, b, user_public_key, user_private_key):
|
def test_assign_transaction_multiple_nodes(self, b, user_public_key, user_private_key):
|
||||||
# create 5 federation nodes
|
# create 5 federation nodes
|
||||||
for _ in range(5):
|
for _ in range(5):
|
||||||
b.federation_nodes.append(b.generate_keys()[1])
|
b.federation_nodes.append(generate_key_pair()[1])
|
||||||
create_inputs(user_public_key, amount=20, b=b)
|
|
||||||
|
|
||||||
# test assignee for several transactions
|
# test assignee for several transactions
|
||||||
for _ in range(20):
|
for _ in range(20):
|
||||||
@ -210,11 +185,11 @@ class TestBigchainApi(object):
|
|||||||
|
|
||||||
def test_create_new_block(self, b):
|
def test_create_new_block(self, b):
|
||||||
new_block = b.create_block([])
|
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']['voters'] == [b.me]
|
||||||
assert new_block['block']['node_pubkey'] == 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['id'] == block_hash
|
||||||
assert new_block['votes'] == []
|
assert new_block['votes'] == []
|
||||||
|
|
||||||
@ -389,13 +364,13 @@ class TestBlockValidation(object):
|
|||||||
|
|
||||||
# create a block with invalid transactions
|
# create a block with invalid transactions
|
||||||
block = {
|
block = {
|
||||||
'timestamp': b.timestamp(),
|
'timestamp': util.timestamp(),
|
||||||
'transactions': [tx_invalid],
|
'transactions': [tx_invalid],
|
||||||
'node_pubkey': b.me,
|
'node_pubkey': b.me,
|
||||||
'voters': b.federation_nodes
|
'voters': b.federation_nodes
|
||||||
}
|
}
|
||||||
|
|
||||||
block_data = b.serialize(block)
|
block_data = util.serialize(block)
|
||||||
block_hash = hash_data(block_data)
|
block_hash = hash_data(block_data)
|
||||||
block_signature = PrivateKey(b.me_private).sign(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']['is_block_valid'] is True
|
||||||
assert vote['vote']['invalid_reason'] is None
|
assert vote['vote']['invalid_reason'] is None
|
||||||
assert vote['node_pubkey'] == b.me
|
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):
|
def test_invalid_block_voting(self, b, user_public_key):
|
||||||
# create queue and voter
|
# create queue and voter
|
||||||
@ -549,7 +524,7 @@ class TestBigchainVoter(object):
|
|||||||
assert vote['vote']['is_block_valid'] is False
|
assert vote['vote']['is_block_valid'] is False
|
||||||
assert vote['vote']['invalid_reason'] is None
|
assert vote['vote']['invalid_reason'] is None
|
||||||
assert vote['node_pubkey'] == b.me
|
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):
|
def test_vote_creation_valid(self, b):
|
||||||
# create valid block
|
# create valid block
|
||||||
@ -563,7 +538,7 @@ class TestBigchainVoter(object):
|
|||||||
assert vote['vote']['is_block_valid'] is True
|
assert vote['vote']['is_block_valid'] is True
|
||||||
assert vote['vote']['invalid_reason'] is None
|
assert vote['vote']['invalid_reason'] is None
|
||||||
assert vote['node_pubkey'] == b.me
|
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):
|
def test_vote_creation_invalid(self, b):
|
||||||
# create valid block
|
# create valid block
|
||||||
@ -577,7 +552,7 @@ class TestBigchainVoter(object):
|
|||||||
assert vote['vote']['is_block_valid'] is False
|
assert vote['vote']['is_block_valid'] is False
|
||||||
assert vote['vote']['invalid_reason'] is None
|
assert vote['vote']['invalid_reason'] is None
|
||||||
assert vote['node_pubkey'] == b.me
|
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):
|
class TestBigchainBlock(object):
|
||||||
|
@ -4,6 +4,7 @@ import pytest
|
|||||||
import rethinkdb as r
|
import rethinkdb as r
|
||||||
|
|
||||||
import bigchaindb
|
import bigchaindb
|
||||||
|
from bigchaindb import util
|
||||||
from bigchaindb.db import utils
|
from bigchaindb.db import utils
|
||||||
from .conftest import setup_database as _setup_database
|
from .conftest import setup_database as _setup_database
|
||||||
|
|
||||||
|
@ -3,8 +3,10 @@ import time
|
|||||||
import rethinkdb as r
|
import rethinkdb as r
|
||||||
import multiprocessing as mp
|
import multiprocessing as mp
|
||||||
|
|
||||||
|
from bigchaindb import util
|
||||||
|
|
||||||
from bigchaindb.voter import Voter, BlockStream
|
from bigchaindb.voter import Voter, BlockStream
|
||||||
from bigchaindb.crypto import PublicKey
|
from bigchaindb.crypto import PublicKey, generate_key_pair
|
||||||
|
|
||||||
|
|
||||||
class TestBigchainVoter(object):
|
class TestBigchainVoter(object):
|
||||||
@ -43,7 +45,7 @@ class TestBigchainVoter(object):
|
|||||||
assert vote['vote']['is_block_valid'] is True
|
assert vote['vote']['is_block_valid'] is True
|
||||||
assert vote['vote']['invalid_reason'] is None
|
assert vote['vote']['invalid_reason'] is None
|
||||||
assert vote['node_pubkey'] == b.me
|
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):
|
def test_valid_block_voting_with_create_transaction(self, b):
|
||||||
q_new_block = mp.Queue()
|
q_new_block = mp.Queue()
|
||||||
@ -51,7 +53,7 @@ class TestBigchainVoter(object):
|
|||||||
genesis = b.create_genesis_block()
|
genesis = b.create_genesis_block()
|
||||||
|
|
||||||
# create a `CREATE` transaction
|
# 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 = b.create_transaction(b.me, test_user_pub, None, 'CREATE')
|
||||||
tx_signed = b.sign_transaction(tx, b.me_private)
|
tx_signed = b.sign_transaction(tx, b.me_private)
|
||||||
assert b.is_valid_transaction(tx_signed)
|
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']['is_block_valid'] is True
|
||||||
assert vote['vote']['invalid_reason'] is None
|
assert vote['vote']['invalid_reason'] is None
|
||||||
assert vote['node_pubkey'] == b.me
|
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):
|
def test_valid_block_voting_with_transfer_transactions(self, b):
|
||||||
q_new_block = mp.Queue()
|
q_new_block = mp.Queue()
|
||||||
@ -93,7 +95,7 @@ class TestBigchainVoter(object):
|
|||||||
b.create_genesis_block()
|
b.create_genesis_block()
|
||||||
|
|
||||||
# create a `CREATE` transaction
|
# 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 = b.create_transaction(b.me, test_user_pub, None, 'CREATE')
|
||||||
tx_signed = b.sign_transaction(tx, b.me_private)
|
tx_signed = b.sign_transaction(tx, b.me_private)
|
||||||
assert b.is_valid_transaction(tx_signed)
|
assert b.is_valid_transaction(tx_signed)
|
||||||
@ -122,7 +124,7 @@ class TestBigchainVoter(object):
|
|||||||
assert len(blocks[1]['votes']) == 1
|
assert len(blocks[1]['votes']) == 1
|
||||||
|
|
||||||
# create a `TRANSFER` transaction
|
# 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 = b.create_transaction(test_user_pub, test_user2_pub, tx['id'], 'TRANSFER')
|
||||||
tx2_signed = b.sign_transaction(tx2, test_user_priv)
|
tx2_signed = b.sign_transaction(tx2, test_user_priv)
|
||||||
assert b.is_valid_transaction(tx2_signed)
|
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']['is_block_valid'] is True
|
||||||
assert vote['vote']['invalid_reason'] is None
|
assert vote['vote']['invalid_reason'] is None
|
||||||
assert vote['node_pubkey'] == b.me
|
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):
|
def test_invalid_block_voting(self, b, user_public_key):
|
||||||
# create queue and voter
|
# create queue and voter
|
||||||
@ -195,7 +197,7 @@ class TestBigchainVoter(object):
|
|||||||
assert vote['vote']['is_block_valid'] is False
|
assert vote['vote']['is_block_valid'] is False
|
||||||
assert vote['vote']['invalid_reason'] is None
|
assert vote['vote']['invalid_reason'] is None
|
||||||
assert vote['node_pubkey'] == b.me
|
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):
|
def test_vote_creation_valid(self, b):
|
||||||
# create valid block
|
# create valid block
|
||||||
@ -209,7 +211,7 @@ class TestBigchainVoter(object):
|
|||||||
assert vote['vote']['is_block_valid'] is True
|
assert vote['vote']['is_block_valid'] is True
|
||||||
assert vote['vote']['invalid_reason'] is None
|
assert vote['vote']['invalid_reason'] is None
|
||||||
assert vote['node_pubkey'] == b.me
|
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):
|
def test_vote_creation_invalid(self, b):
|
||||||
# create valid block
|
# create valid block
|
||||||
@ -223,7 +225,7 @@ class TestBigchainVoter(object):
|
|||||||
assert vote['vote']['is_block_valid'] is False
|
assert vote['vote']['is_block_valid'] is False
|
||||||
assert vote['vote']['invalid_reason'] is None
|
assert vote['vote']['invalid_reason'] is None
|
||||||
assert vote['node_pubkey'] == b.me
|
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):
|
def test_voter_considers_unvoted_blocks_when_single_node(self, b):
|
||||||
# simulate a voter going donw in a single node environment
|
# 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):
|
def test_if_federation_size_is_greater_than_one_ignore_past_blocks(self, b):
|
||||||
for _ in range(5):
|
for _ in range(5):
|
||||||
b.federation_nodes.append(b.generate_keys()[1])
|
b.federation_nodes.append(generate_key_pair()[1])
|
||||||
new_blocks = mp.Queue()
|
new_blocks = mp.Queue()
|
||||||
bs = BlockStream(new_blocks)
|
bs = BlockStream(new_blocks)
|
||||||
block_1 = b.create_block([])
|
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
|
value['return'] = newconfig
|
||||||
|
|
||||||
monkeypatch.setattr(config_utils, 'write_config', mock_write_config)
|
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)
|
monkeypatch.setattr('os.path.exists', lambda path: False)
|
||||||
|
|
||||||
args = Namespace(config=None, yes=True)
|
args = Namespace(config=None, yes=True)
|
||||||
|
@ -1,10 +1,8 @@
|
|||||||
import copy
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def config(request):
|
def config(request, monkeypatch):
|
||||||
import bigchaindb
|
|
||||||
config = {
|
config = {
|
||||||
'database': {
|
'database': {
|
||||||
'host': 'host',
|
'host': 'host',
|
||||||
@ -18,13 +16,10 @@ def config(request):
|
|||||||
'keyring': [],
|
'keyring': [],
|
||||||
'CONFIGURED': True,
|
'CONFIGURED': True,
|
||||||
}
|
}
|
||||||
bigchaindb.config.update(config)
|
|
||||||
|
|
||||||
def fin():
|
monkeypatch.setattr('bigchaindb.config', config)
|
||||||
bigchaindb.config = bigchaindb._config
|
|
||||||
bigchaindb._config = copy.deepcopy(bigchaindb._config)
|
return config
|
||||||
request.addfinalizer(fin)
|
|
||||||
return bigchaindb.config
|
|
||||||
|
|
||||||
|
|
||||||
def test_bigchain_class_default_initialization(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 pytest
|
||||||
|
|
||||||
import bigchaindb
|
import bigchaindb
|
||||||
|
from bigchaindb import exceptions
|
||||||
|
|
||||||
|
|
||||||
ORIGINAL_CONFIG = copy.deepcopy(bigchaindb._config)
|
ORIGINAL_CONFIG = copy.deepcopy(bigchaindb._config)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope='function', autouse=True)
|
@pytest.fixture(scope='function', autouse=True)
|
||||||
def clean_config():
|
def clean_config(monkeypatch):
|
||||||
bigchaindb.config = copy.deepcopy(ORIGINAL_CONFIG)
|
monkeypatch.setattr('bigchaindb.config', copy.deepcopy(ORIGINAL_CONFIG))
|
||||||
|
|
||||||
|
|
||||||
def test_bigchain_instance_is_initialized_when_conf_provided():
|
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
|
# from existing configurations
|
||||||
monkeypatch.setattr(config_utils, 'autoconfigure', lambda: 0)
|
monkeypatch.setattr(config_utils, 'autoconfigure', lambda: 0)
|
||||||
|
|
||||||
with pytest.raises(bigchaindb.core.KeypairNotFoundException):
|
with pytest.raises(exceptions.KeypairNotFoundException):
|
||||||
bigchaindb.Bigchain()
|
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