mirror of
https://github.com/bigchaindb/bigchaindb.git
synced 2024-10-13 13:34:05 +00:00
Flat UTXO collection and first integration with Tendermint™ (#1822)
* Remove testing for rethinkdb, mongodb, and Py3.5 * Add first tests * Add validation * Add command to start the ABCI Server * Reuse existing MongoDB Connection class * Use DuplicateTransaction * Test only tendermint * Update travis scripts * Fix pep8 errors * Update Makefile
This commit is contained in:
parent
b4738e2e61
commit
2815cffcb5
@ -2,30 +2,26 @@
|
|||||||
|
|
||||||
set -e -x
|
set -e -x
|
||||||
|
|
||||||
if [[ "${BIGCHAINDB_DATABASE_BACKEND}" == rethinkdb ]]; then
|
if [[ "${BIGCHAINDB_DATABASE_BACKEND}" == localmongodb && \
|
||||||
docker pull rethinkdb:2.3.5
|
|
||||||
docker run -d --publish=28015:28015 --name rdb rethinkdb:2.3.5
|
|
||||||
elif [[ "${BIGCHAINDB_DATABASE_BACKEND}" == mongodb && \
|
|
||||||
-z "${BIGCHAINDB_DATABASE_SSL}" ]]; then
|
-z "${BIGCHAINDB_DATABASE_SSL}" ]]; then
|
||||||
# Connect to MongoDB on port 27017 via a normal, unsecure connection if
|
# Connect to MongoDB on port 27017 via a normal, unsecure connection if
|
||||||
# BIGCHAINDB_DATABASE_SSL is unset.
|
# BIGCHAINDB_DATABASE_SSL is unset.
|
||||||
# It is unset in this case in .travis.yml.
|
# It is unset in this case in .travis.yml.
|
||||||
docker pull mongo:3.4.4
|
docker pull mongo:3.4
|
||||||
docker run -d --publish=27017:27017 --name mdb-without-ssl mongo:3.4.4 \
|
docker run -d --publish=27017:27017 --name mdb-without-ssl mongo:3.4 # --replSet=bigchain-rs
|
||||||
--replSet=bigchain-rs
|
elif [[ "${BIGCHAINDB_DATABASE_BACKEND}" == localmongodb && \
|
||||||
elif [[ "${BIGCHAINDB_DATABASE_BACKEND}" == mongodb && \
|
|
||||||
"${BIGCHAINDB_DATABASE_SSL}" == true ]]; then
|
"${BIGCHAINDB_DATABASE_SSL}" == true ]]; then
|
||||||
# Connect to MongoDB on port 27017 via TLS/SSL connection if
|
# Connect to MongoDB on port 27017 via TLS/SSL connection if
|
||||||
# BIGCHAINDB_DATABASE_SSL is set.
|
# BIGCHAINDB_DATABASE_SSL is set.
|
||||||
# It is set to 'true' here in .travis.yml. Dummy certificates for testing
|
# It is set to 'true' here in .travis.yml. Dummy certificates for testing
|
||||||
# are stored under bigchaindb/tests/backend/mongodb-ssl/certs/ directory.
|
# are stored under bigchaindb/tests/backend/mongodb-ssl/certs/ directory.
|
||||||
docker pull mongo:3.4.4
|
docker pull mongo:3.4
|
||||||
docker run -d \
|
docker run -d \
|
||||||
--name mdb-with-ssl \
|
--name mdb-with-ssl \
|
||||||
--publish=27017:27017 \
|
--publish=27017:27017 \
|
||||||
--volume=${TRAVIS_BUILD_DIR}/tests/backend/mongodb-ssl/certs:/certs \
|
--volume=${TRAVIS_BUILD_DIR}/tests/backend/mongodb-ssl/certs:/certs \
|
||||||
mongo:3.4.4 \
|
mongo:3.4 \
|
||||||
--replSet=bigchain-rs \
|
# --replSet=bigchain-rs \
|
||||||
--sslAllowInvalidHostnames \
|
--sslAllowInvalidHostnames \
|
||||||
--sslMode=requireSSL \
|
--sslMode=requireSSL \
|
||||||
--sslCAFile=/certs/ca.crt \
|
--sslCAFile=/certs/ca.crt \
|
||||||
|
@ -4,14 +4,14 @@ set -e -x
|
|||||||
|
|
||||||
if [[ -n ${TOXENV} ]]; then
|
if [[ -n ${TOXENV} ]]; then
|
||||||
tox -e ${TOXENV}
|
tox -e ${TOXENV}
|
||||||
elif [[ "${BIGCHAINDB_DATABASE_BACKEND}" == mongodb && \
|
elif [[ "${BIGCHAINDB_DATABASE_BACKEND}" == localmongodb && \
|
||||||
-z "${BIGCHAINDB_DATABASE_SSL}" ]]; then
|
-z "${BIGCHAINDB_DATABASE_SSL}" ]]; then
|
||||||
# Run the full suite of tests for MongoDB over an unsecure connection
|
# Run the full suite of tests for MongoDB over an unsecure connection
|
||||||
pytest -sv --database-backend=mongodb --cov=bigchaindb
|
pytest -sv --database-backend=localmongodb --cov=bigchaindb
|
||||||
elif [[ "${BIGCHAINDB_DATABASE_BACKEND}" == mongodb && \
|
elif [[ "${BIGCHAINDB_DATABASE_BACKEND}" == localmongodb && \
|
||||||
"${BIGCHAINDB_DATABASE_SSL}" == true ]]; then
|
"${BIGCHAINDB_DATABASE_SSL}" == true ]]; then
|
||||||
# Run a sub-set of tests over SSL; those marked as 'pytest.mark.bdb_ssl'.
|
# Run a sub-set of tests over SSL; those marked as 'pytest.mark.bdb_ssl'.
|
||||||
pytest -sv --database-backend=mongodb-ssl --cov=bigchaindb -m bdb_ssl
|
pytest -sv --database-backend=localmongodb-ssl --cov=bigchaindb -m bdb_ssl
|
||||||
else
|
else
|
||||||
pytest -sv -n auto --cov=bigchaindb
|
pytest -sv -n auto --cov=bigchaindb
|
||||||
fi
|
fi
|
||||||
|
28
.travis.yml
28
.travis.yml
@ -9,7 +9,6 @@ language: python
|
|||||||
cache: pip
|
cache: pip
|
||||||
|
|
||||||
python:
|
python:
|
||||||
- 3.5
|
|
||||||
- 3.6
|
- 3.6
|
||||||
|
|
||||||
env:
|
env:
|
||||||
@ -19,34 +18,11 @@ env:
|
|||||||
|
|
||||||
matrix:
|
matrix:
|
||||||
fast_finish: true
|
fast_finish: true
|
||||||
exclude:
|
|
||||||
- python: 3.5
|
|
||||||
env: TOXENV=flake8
|
|
||||||
- python: 3.5
|
|
||||||
env: TOXENV=docsroot
|
|
||||||
- python: 3.5
|
|
||||||
env: TOXENV=docsserver
|
|
||||||
include:
|
include:
|
||||||
- python: 3.5
|
- python: 3.6
|
||||||
env: BIGCHAINDB_DATABASE_BACKEND=rethinkdb
|
|
||||||
- python: 3.5
|
|
||||||
env:
|
env:
|
||||||
- BIGCHAINDB_DATABASE_BACKEND=mongodb
|
- BIGCHAINDB_DATABASE_BACKEND=localmongodb
|
||||||
- BIGCHAINDB_DATABASE_SSL=
|
- BIGCHAINDB_DATABASE_SSL=
|
||||||
- python: 3.6
|
|
||||||
env: BIGCHAINDB_DATABASE_BACKEND=rethinkdb
|
|
||||||
- python: 3.6
|
|
||||||
env:
|
|
||||||
- BIGCHAINDB_DATABASE_BACKEND=mongodb
|
|
||||||
- BIGCHAINDB_DATABASE_SSL=
|
|
||||||
- python: 3.5
|
|
||||||
env:
|
|
||||||
- BIGCHAINDB_DATABASE_BACKEND=mongodb
|
|
||||||
- BIGCHAINDB_DATABASE_SSL=true
|
|
||||||
- python: 3.6
|
|
||||||
env:
|
|
||||||
- BIGCHAINDB_DATABASE_BACKEND=mongodb
|
|
||||||
- BIGCHAINDB_DATABASE_SSL=true
|
|
||||||
|
|
||||||
before_install: sudo .ci/travis-before-install.sh
|
before_install: sudo .ci/travis-before-install.sh
|
||||||
|
|
||||||
|
2
Makefile
2
Makefile
@ -57,7 +57,7 @@ test-all: ## run tests on every Python version with tox
|
|||||||
tox
|
tox
|
||||||
|
|
||||||
coverage: ## check code coverage quickly with the default Python
|
coverage: ## check code coverage quickly with the default Python
|
||||||
pytest -v -n auto --cov=bigchaindb --cov-report term --cov-report html
|
pytest -v -n auto --database-backend=localmongodb --cov=bigchaindb --cov-report term --cov-report html
|
||||||
$(BROWSER) htmlcov/index.html
|
$(BROWSER) htmlcov/index.html
|
||||||
|
|
||||||
docs: ## generate Sphinx HTML documentation, including API docs
|
docs: ## generate Sphinx HTML documentation, including API docs
|
||||||
|
@ -21,10 +21,20 @@ _base_database_rethinkdb = {
|
|||||||
# because dicts are unordered. I tried to configure
|
# because dicts are unordered. I tried to configure
|
||||||
|
|
||||||
_database_keys_map = {
|
_database_keys_map = {
|
||||||
|
'localmongodb': ('host', 'port', 'name'),
|
||||||
'mongodb': ('host', 'port', 'name', 'replicaset'),
|
'mongodb': ('host', 'port', 'name', 'replicaset'),
|
||||||
'rethinkdb': ('host', 'port', 'name')
|
'rethinkdb': ('host', 'port', 'name')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_base_database_localmongodb = {
|
||||||
|
'host': os.environ.get('BIGCHAINDB_DATABASE_HOST', 'localhost'),
|
||||||
|
'port': int(os.environ.get('BIGCHAINDB_DATABASE_PORT', 27017)),
|
||||||
|
'name': os.environ.get('BIGCHAINDB_DATABASE_NAME', 'bigchain'),
|
||||||
|
'replicaset': os.environ.get('BIGCHAINDB_DATABASE_REPLICASET'),
|
||||||
|
'login': os.environ.get('BIGCHAINDB_DATABASE_LOGIN'),
|
||||||
|
'password': os.environ.get('BIGCHAINDB_DATABASE_PASSWORD')
|
||||||
|
}
|
||||||
|
|
||||||
_base_database_mongodb = {
|
_base_database_mongodb = {
|
||||||
'host': os.environ.get('BIGCHAINDB_DATABASE_HOST', 'localhost'),
|
'host': os.environ.get('BIGCHAINDB_DATABASE_HOST', 'localhost'),
|
||||||
'port': int(os.environ.get('BIGCHAINDB_DATABASE_PORT', 27017)),
|
'port': int(os.environ.get('BIGCHAINDB_DATABASE_PORT', 27017)),
|
||||||
@ -54,7 +64,21 @@ _database_mongodb = {
|
|||||||
}
|
}
|
||||||
_database_mongodb.update(_base_database_mongodb)
|
_database_mongodb.update(_base_database_mongodb)
|
||||||
|
|
||||||
|
_database_localmongodb = {
|
||||||
|
'backend': os.environ.get('BIGCHAINDB_DATABASE_BACKEND', 'localmongodb'),
|
||||||
|
'connection_timeout': 5000,
|
||||||
|
'max_tries': 3,
|
||||||
|
'ssl': bool(os.environ.get('BIGCHAINDB_DATABASE_SSL', False)),
|
||||||
|
'ca_cert': os.environ.get('BIGCHAINDB_DATABASE_CA_CERT'),
|
||||||
|
'certfile': os.environ.get('BIGCHAINDB_DATABASE_CERTFILE'),
|
||||||
|
'keyfile': os.environ.get('BIGCHAINDB_DATABASE_KEYFILE'),
|
||||||
|
'keyfile_passphrase': os.environ.get('BIGCHAINDB_DATABASE_KEYFILE_PASSPHRASE'),
|
||||||
|
'crlfile': os.environ.get('BIGCHAINDB_DATABASE_CRLFILE')
|
||||||
|
}
|
||||||
|
_database_localmongodb.update(_base_database_localmongodb)
|
||||||
|
|
||||||
_database_map = {
|
_database_map = {
|
||||||
|
'localmongodb': _database_localmongodb,
|
||||||
'mongodb': _database_mongodb,
|
'mongodb': _database_mongodb,
|
||||||
'rethinkdb': _database_rethinkdb
|
'rethinkdb': _database_rethinkdb
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ from bigchaindb.backend.exceptions import ConnectionError
|
|||||||
|
|
||||||
|
|
||||||
BACKENDS = {
|
BACKENDS = {
|
||||||
|
'localmongodb': 'bigchaindb.backend.localmongodb.connection.LocalMongoDBConnection',
|
||||||
'mongodb': 'bigchaindb.backend.mongodb.connection.MongoDBConnection',
|
'mongodb': 'bigchaindb.backend.mongodb.connection.MongoDBConnection',
|
||||||
'rethinkdb': 'bigchaindb.backend.rethinkdb.connection.RethinkDBConnection'
|
'rethinkdb': 'bigchaindb.backend.rethinkdb.connection.RethinkDBConnection'
|
||||||
}
|
}
|
||||||
|
22
bigchaindb/backend/localmongodb/__init__.py
Normal file
22
bigchaindb/backend/localmongodb/__init__.py
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
"""MongoDB backend implementation.
|
||||||
|
|
||||||
|
Contains a MongoDB-specific implementation of the
|
||||||
|
:mod:`~bigchaindb.backend.changefeed`, :mod:`~bigchaindb.backend.query`, and
|
||||||
|
:mod:`~bigchaindb.backend.schema` interfaces.
|
||||||
|
|
||||||
|
You can specify BigchainDB to use MongoDB as its database backend by either
|
||||||
|
setting ``database.backend`` to ``'rethinkdb'`` in your configuration file, or
|
||||||
|
setting the ``BIGCHAINDB_DATABASE_BACKEND`` environment variable to
|
||||||
|
``'rethinkdb'``.
|
||||||
|
|
||||||
|
If configured to use MongoDB, BigchainDB will automatically return instances
|
||||||
|
of :class:`~bigchaindb.backend.rethinkdb.MongoDBConnection` for
|
||||||
|
:func:`~bigchaindb.backend.connection.connect` and dispatch calls of the
|
||||||
|
generic backend interfaces to the implementations in this module.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Register the single dispatched modules on import.
|
||||||
|
from bigchaindb.backend.localmongodb import schema, query # noqa
|
||||||
|
|
||||||
|
# MongoDBConnection should always be accessed via
|
||||||
|
# ``bigchaindb.backend.connect()``.
|
5
bigchaindb/backend/localmongodb/connection.py
Normal file
5
bigchaindb/backend/localmongodb/connection.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
from bigchaindb.backend.mongodb.connection import MongoDBConnection
|
||||||
|
|
||||||
|
|
||||||
|
class LocalMongoDBConnection(MongoDBConnection):
|
||||||
|
pass
|
41
bigchaindb/backend/localmongodb/query.py
Normal file
41
bigchaindb/backend/localmongodb/query.py
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
"""Query implementation for MongoDB"""
|
||||||
|
|
||||||
|
from bigchaindb import backend
|
||||||
|
from bigchaindb.backend.exceptions import DuplicateKeyError
|
||||||
|
from bigchaindb.backend.utils import module_dispatch_registrar
|
||||||
|
from bigchaindb.backend.localmongodb.connection import LocalMongoDBConnection
|
||||||
|
|
||||||
|
|
||||||
|
register_query = module_dispatch_registrar(backend.query)
|
||||||
|
|
||||||
|
|
||||||
|
@register_query(LocalMongoDBConnection)
|
||||||
|
def write_transaction(conn, signed_transaction):
|
||||||
|
try:
|
||||||
|
return conn.run(
|
||||||
|
conn.collection('transactions')
|
||||||
|
.insert_one(signed_transaction))
|
||||||
|
except DuplicateKeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@register_query(LocalMongoDBConnection)
|
||||||
|
def get_transaction(conn, transaction_id):
|
||||||
|
try:
|
||||||
|
return conn.run(
|
||||||
|
conn.collection('transactions')
|
||||||
|
.find_one({'id': transaction_id}, {'_id': 0}))
|
||||||
|
except IndexError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@register_query(LocalMongoDBConnection)
|
||||||
|
def get_spent(conn, transaction_id, output):
|
||||||
|
try:
|
||||||
|
return conn.run(
|
||||||
|
conn.collection('transactions')
|
||||||
|
.find_one({'id': transaction_id,
|
||||||
|
'inputs.fulfills.output_index': output},
|
||||||
|
{'_id': 0}))
|
||||||
|
except IndexError:
|
||||||
|
pass
|
82
bigchaindb/backend/localmongodb/schema.py
Normal file
82
bigchaindb/backend/localmongodb/schema.py
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
"""Utils to initialize and drop the database."""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from pymongo import ASCENDING, TEXT
|
||||||
|
|
||||||
|
from bigchaindb import backend
|
||||||
|
from bigchaindb.common import exceptions
|
||||||
|
from bigchaindb.backend.utils import module_dispatch_registrar
|
||||||
|
from bigchaindb.backend.localmongodb.connection import LocalMongoDBConnection
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
register_schema = module_dispatch_registrar(backend.schema)
|
||||||
|
|
||||||
|
|
||||||
|
@register_schema(LocalMongoDBConnection)
|
||||||
|
def create_database(conn, dbname):
|
||||||
|
if dbname in conn.conn.database_names():
|
||||||
|
raise exceptions.DatabaseAlreadyExists('Database `{}` already exists'
|
||||||
|
.format(dbname))
|
||||||
|
|
||||||
|
logger.info('Create database `%s`.', dbname)
|
||||||
|
# TODO: read and write concerns can be declared here
|
||||||
|
conn.conn.get_database(dbname)
|
||||||
|
|
||||||
|
|
||||||
|
@register_schema(LocalMongoDBConnection)
|
||||||
|
def create_tables(conn, dbname):
|
||||||
|
for table_name in ['transactions', 'assets']:
|
||||||
|
logger.info('Create `%s` table.', table_name)
|
||||||
|
# create the table
|
||||||
|
# TODO: read and write concerns can be declared here
|
||||||
|
conn.conn[dbname].create_collection(table_name)
|
||||||
|
|
||||||
|
|
||||||
|
@register_schema(LocalMongoDBConnection)
|
||||||
|
def create_indexes(conn, dbname):
|
||||||
|
create_transactions_secondary_index(conn, dbname)
|
||||||
|
create_assets_secondary_index(conn, dbname)
|
||||||
|
|
||||||
|
|
||||||
|
@register_schema(LocalMongoDBConnection)
|
||||||
|
def drop_database(conn, dbname):
|
||||||
|
conn.conn.drop_database(dbname)
|
||||||
|
|
||||||
|
|
||||||
|
def create_transactions_secondary_index(conn, dbname):
|
||||||
|
logger.info('Create `transactions` secondary index.')
|
||||||
|
|
||||||
|
# to query the transactions for a transaction id, this field is unique
|
||||||
|
conn.conn[dbname]['transactions'].create_index('transactions.id',
|
||||||
|
name='transaction_id')
|
||||||
|
|
||||||
|
# secondary index for asset uuid, this field is unique
|
||||||
|
conn.conn[dbname]['transactions']\
|
||||||
|
.create_index('asset.id', name='asset_id')
|
||||||
|
|
||||||
|
# secondary index on the public keys of outputs
|
||||||
|
conn.conn[dbname]['transactions']\
|
||||||
|
.create_index('outputs.public_keys',
|
||||||
|
name='outputs')
|
||||||
|
|
||||||
|
# secondary index on inputs/transaction links (transaction_id, output)
|
||||||
|
conn.conn[dbname]['transactions']\
|
||||||
|
.create_index([
|
||||||
|
('inputs.fulfills.transaction_id', ASCENDING),
|
||||||
|
('inputs.fulfills.output_index', ASCENDING),
|
||||||
|
], name='inputs')
|
||||||
|
|
||||||
|
|
||||||
|
def create_assets_secondary_index(conn, dbname):
|
||||||
|
logger.info('Create `assets` secondary index.')
|
||||||
|
|
||||||
|
# unique index on the id of the asset.
|
||||||
|
# the id is the txid of the transaction that created the asset
|
||||||
|
conn.conn[dbname]['assets'].create_index('id',
|
||||||
|
name='asset_id',
|
||||||
|
unique=True)
|
||||||
|
|
||||||
|
# full text search index
|
||||||
|
conn.conn[dbname]['assets'].create_index([('$**', TEXT)], name='text')
|
@ -84,21 +84,22 @@ class MongoDBConnection(Connection):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# we should only return a connection if the replica set is
|
if self.replicaset:
|
||||||
# initialized. initialize_replica_set will check if the
|
# we should only return a connection if the replica set is
|
||||||
# replica set is initialized else it will initialize it.
|
# initialized. initialize_replica_set will check if the
|
||||||
initialize_replica_set(self.host,
|
# replica set is initialized else it will initialize it.
|
||||||
self.port,
|
initialize_replica_set(self.host,
|
||||||
self.connection_timeout,
|
self.port,
|
||||||
self.dbname,
|
self.connection_timeout,
|
||||||
self.ssl,
|
self.dbname,
|
||||||
self.login,
|
self.ssl,
|
||||||
self.password,
|
self.login,
|
||||||
self.ca_cert,
|
self.password,
|
||||||
self.certfile,
|
self.ca_cert,
|
||||||
self.keyfile,
|
self.certfile,
|
||||||
self.keyfile_passphrase,
|
self.keyfile,
|
||||||
self.crlfile)
|
self.keyfile_passphrase,
|
||||||
|
self.crlfile)
|
||||||
|
|
||||||
# FYI: the connection process might raise a
|
# FYI: the connection process might raise a
|
||||||
# `ServerSelectionTimeoutError`, that is a subclass of
|
# `ServerSelectionTimeoutError`, that is a subclass of
|
||||||
|
@ -19,6 +19,20 @@ def write_transaction(connection, signed_transaction):
|
|||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
@singledispatch
|
||||||
|
def get_transaction(connection, signed_transaction):
|
||||||
|
"""Get a transaction from the transactions table.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
signed_transaction (dict): a signed transaction.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The result of the operation.
|
||||||
|
"""
|
||||||
|
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
@singledispatch
|
@singledispatch
|
||||||
def update_transaction(connection, transaction_id, doc):
|
def update_transaction(connection, transaction_id, doc):
|
||||||
"""Update a transaction in the backlog table.
|
"""Update a transaction in the backlog table.
|
||||||
|
@ -270,7 +270,7 @@ def create_parser():
|
|||||||
help='Prepare the config file '
|
help='Prepare the config file '
|
||||||
'and create the node keypair')
|
'and create the node keypair')
|
||||||
config_parser.add_argument('backend',
|
config_parser.add_argument('backend',
|
||||||
choices=['rethinkdb', 'mongodb'],
|
choices=['rethinkdb', 'mongodb', 'localmongodb'],
|
||||||
help='The backend to use. It can be either '
|
help='The backend to use. It can be either '
|
||||||
'rethinkdb or mongodb.')
|
'rethinkdb or mongodb.')
|
||||||
|
|
||||||
|
@ -30,7 +30,11 @@ class Transaction(Transaction):
|
|||||||
"""
|
"""
|
||||||
input_conditions = []
|
input_conditions = []
|
||||||
|
|
||||||
if self.operation == Transaction.TRANSFER:
|
if self.operation == Transaction.CREATE:
|
||||||
|
if bigchain.get_transaction(self.to_dict()['id']):
|
||||||
|
raise DuplicateTransaction('transaction `{}` already exists'
|
||||||
|
.format(self.id))
|
||||||
|
elif self.operation == Transaction.TRANSFER:
|
||||||
# store the inputs so that we can check if the asset ids match
|
# store the inputs so that we can check if the asset ids match
|
||||||
input_txs = []
|
input_txs = []
|
||||||
for input_ in self.inputs:
|
for input_ in self.inputs:
|
||||||
|
5
bigchaindb/tendermint/__init__.py
Normal file
5
bigchaindb/tendermint/__init__.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# Order is important!
|
||||||
|
# If we import core first, core will try to load BigchainDB from
|
||||||
|
# __init__ itself, causing a loop.
|
||||||
|
from bigchaindb.tendermint.lib import BigchainDB # noqa
|
||||||
|
from bigchaindb.tendermint.core import App # noqa
|
12
bigchaindb/tendermint/commands.py
Normal file
12
bigchaindb/tendermint/commands.py
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
from abci import ABCIServer
|
||||||
|
|
||||||
|
from bigchaindb.tendermint.core import App
|
||||||
|
|
||||||
|
|
||||||
|
def start():
|
||||||
|
app = ABCIServer(app=App())
|
||||||
|
app.run()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
start()
|
49
bigchaindb/tendermint/core.py
Normal file
49
bigchaindb/tendermint/core.py
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
"""This module contains all the goodness to integrate BigchainDB
|
||||||
|
with Tendermint."""
|
||||||
|
|
||||||
|
|
||||||
|
from abci import BaseApplication, Result
|
||||||
|
|
||||||
|
from bigchaindb.tendermint import BigchainDB
|
||||||
|
from bigchaindb.tendermint.utils import decode_transaction
|
||||||
|
|
||||||
|
|
||||||
|
class App(BaseApplication):
|
||||||
|
"""Bridge between BigchainDB and Tendermint.
|
||||||
|
|
||||||
|
The role of this class is to expose the BigchainDB
|
||||||
|
transactional logic to the Tendermint Consensus
|
||||||
|
State Machine."""
|
||||||
|
|
||||||
|
def __init__(self, bigchaindb=None):
|
||||||
|
if not bigchaindb:
|
||||||
|
bigchaindb = BigchainDB()
|
||||||
|
self.bigchaindb = bigchaindb
|
||||||
|
|
||||||
|
def check_tx(self, raw_transaction):
|
||||||
|
"""Validate the transaction before entry into
|
||||||
|
the mempool.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
raw_tx: an encoded transaction."""
|
||||||
|
|
||||||
|
transaction = decode_transaction(raw_transaction)
|
||||||
|
if self.bigchaindb.validate_transaction(transaction):
|
||||||
|
return Result.ok()
|
||||||
|
else:
|
||||||
|
return Result.error()
|
||||||
|
|
||||||
|
def deliver_tx(self, raw_transaction):
|
||||||
|
"""Validate the transaction before mutating the state.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
raw_tx: an encoded transaction."""
|
||||||
|
|
||||||
|
transaction = self.bigchaindb.validate_transaction(
|
||||||
|
decode_transaction(raw_transaction))
|
||||||
|
|
||||||
|
if not transaction:
|
||||||
|
return Result.error(log='Invalid transaction')
|
||||||
|
else:
|
||||||
|
self.bigchaindb.write_transaction(transaction)
|
||||||
|
return Result.ok()
|
52
bigchaindb/tendermint/lib.py
Normal file
52
bigchaindb/tendermint/lib.py
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
from bigchaindb import backend
|
||||||
|
from bigchaindb import Bigchain
|
||||||
|
from bigchaindb.models import Transaction
|
||||||
|
from bigchaindb.common.exceptions import SchemaValidationError, ValidationError
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class BigchainDB(Bigchain):
|
||||||
|
|
||||||
|
def write_transaction(self, transaction):
|
||||||
|
"""Write a valid transaction to the transactions collection."""
|
||||||
|
|
||||||
|
return backend.query.write_transaction(self.connection, transaction.to_dict())
|
||||||
|
|
||||||
|
def get_transaction(self, transaction, include_status=False):
|
||||||
|
result = backend.query.get_transaction(self.connection, transaction)
|
||||||
|
|
||||||
|
if result:
|
||||||
|
result = Transaction.from_dict(result)
|
||||||
|
|
||||||
|
if include_status:
|
||||||
|
return result, self.TX_VALID if result else None
|
||||||
|
else:
|
||||||
|
return result
|
||||||
|
|
||||||
|
def get_spent(self, txid, output):
|
||||||
|
transaction = backend.query.get_spent(self.connection, txid, output)
|
||||||
|
return Transaction.from_dict(transaction)
|
||||||
|
|
||||||
|
def validate_transaction(self, tx):
|
||||||
|
"""Validate a transaction against the current status
|
||||||
|
of the database."""
|
||||||
|
|
||||||
|
try:
|
||||||
|
tx_obj = Transaction.from_dict(tx)
|
||||||
|
except SchemaValidationError as e:
|
||||||
|
logger.warning('Invalid transaction schema: %s', e.__cause__.message)
|
||||||
|
return False
|
||||||
|
except ValidationError as e:
|
||||||
|
logger.warning('Invalid transaction (%s): %s', type(e).__name__, e)
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
return tx_obj.validate(self)
|
||||||
|
except ValidationError as e:
|
||||||
|
logger.warning('Invalid transaction (%s): %s', type(e).__name__, e)
|
||||||
|
return False
|
||||||
|
return tx_obj
|
14
bigchaindb/tendermint/utils.py
Normal file
14
bigchaindb/tendermint/utils.py
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import base64
|
||||||
|
import json
|
||||||
|
|
||||||
|
|
||||||
|
def encode_transaction(value):
|
||||||
|
"""Encode a transaction to Base64."""
|
||||||
|
|
||||||
|
return base64.b64encode(json.dumps(value).encode('utf8'))
|
||||||
|
|
||||||
|
|
||||||
|
def decode_transaction(raw):
|
||||||
|
"""Decode a transaction from Base64 to a dict."""
|
||||||
|
|
||||||
|
return json.loads(base64.b64decode(raw.decode('utf8')))
|
@ -1,3 +1,3 @@
|
|||||||
[pytest]
|
[pytest]
|
||||||
testpaths = tests
|
testpaths = tests/tendermint
|
||||||
norecursedirs = .* *.egg *.egg-info env* devenv* docs
|
norecursedirs = .* *.egg *.egg-info env* devenv* docs
|
||||||
|
1
setup.py
1
setup.py
@ -83,6 +83,7 @@ install_requires = [
|
|||||||
'aiohttp~=2.0',
|
'aiohttp~=2.0',
|
||||||
'python-rapidjson-schema==0.1.1',
|
'python-rapidjson-schema==0.1.1',
|
||||||
'statsd==3.2.1',
|
'statsd==3.2.1',
|
||||||
|
'abci~=0.2.0',
|
||||||
]
|
]
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
|
0
tests/tendermint/__init__.py
Normal file
0
tests/tendermint/__init__.py
Normal file
7
tests/tendermint/conftest.py
Normal file
7
tests/tendermint/conftest.py
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def b():
|
||||||
|
from bigchaindb.tendermint import BigchainDB
|
||||||
|
return BigchainDB()
|
73
tests/tendermint/test_core.py
Normal file
73
tests/tendermint/test_core.py
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
def test_check_tx__signed_create_is_ok(b):
|
||||||
|
from bigchaindb.tendermint import App
|
||||||
|
from bigchaindb.tendermint.utils import encode_transaction
|
||||||
|
from bigchaindb.models import Transaction
|
||||||
|
from bigchaindb.common.crypto import generate_key_pair
|
||||||
|
|
||||||
|
alice = generate_key_pair()
|
||||||
|
bob = generate_key_pair()
|
||||||
|
|
||||||
|
tx = Transaction.create([alice.public_key],
|
||||||
|
[([bob.public_key], 1)])\
|
||||||
|
.sign([alice.private_key])
|
||||||
|
|
||||||
|
app = App(b)
|
||||||
|
result = app.check_tx(encode_transaction(tx.to_dict()))
|
||||||
|
assert result.is_ok()
|
||||||
|
|
||||||
|
|
||||||
|
def test_check_tx__unsigned_create_is_error(b):
|
||||||
|
from bigchaindb.tendermint import App
|
||||||
|
from bigchaindb.tendermint.utils import encode_transaction
|
||||||
|
from bigchaindb.models import Transaction
|
||||||
|
from bigchaindb.common.crypto import generate_key_pair
|
||||||
|
|
||||||
|
alice = generate_key_pair()
|
||||||
|
bob = generate_key_pair()
|
||||||
|
|
||||||
|
tx = Transaction.create([alice.public_key],
|
||||||
|
[([bob.public_key], 1)])
|
||||||
|
|
||||||
|
app = App(b)
|
||||||
|
result = app.check_tx(encode_transaction(tx.to_dict()))
|
||||||
|
assert result.is_error()
|
||||||
|
|
||||||
|
|
||||||
|
def test_deliver_tx__valid_create_updates_db(b):
|
||||||
|
from bigchaindb.tendermint import App
|
||||||
|
from bigchaindb.tendermint.utils import encode_transaction
|
||||||
|
from bigchaindb.models import Transaction
|
||||||
|
from bigchaindb.common.crypto import generate_key_pair
|
||||||
|
|
||||||
|
alice = generate_key_pair()
|
||||||
|
bob = generate_key_pair()
|
||||||
|
|
||||||
|
tx = Transaction.create([alice.public_key],
|
||||||
|
[([bob.public_key], 1)])\
|
||||||
|
.sign([alice.private_key])
|
||||||
|
|
||||||
|
app = App(b)
|
||||||
|
result = app.deliver_tx(encode_transaction(tx.to_dict()))
|
||||||
|
assert result.is_ok()
|
||||||
|
assert b.get_transaction(tx.id).id == tx.id
|
||||||
|
|
||||||
|
|
||||||
|
def test_deliver_tx__double_spend_fails(b):
|
||||||
|
from bigchaindb.tendermint import App
|
||||||
|
from bigchaindb.tendermint.utils import encode_transaction
|
||||||
|
from bigchaindb.models import Transaction
|
||||||
|
from bigchaindb.common.crypto import generate_key_pair
|
||||||
|
|
||||||
|
alice = generate_key_pair()
|
||||||
|
bob = generate_key_pair()
|
||||||
|
|
||||||
|
tx = Transaction.create([alice.public_key],
|
||||||
|
[([bob.public_key], 1)])\
|
||||||
|
.sign([alice.private_key])
|
||||||
|
|
||||||
|
app = App(b)
|
||||||
|
result = app.deliver_tx(encode_transaction(tx.to_dict()))
|
||||||
|
assert result.is_ok()
|
||||||
|
assert b.get_transaction(tx.id).id == tx.id
|
||||||
|
result = app.deliver_tx(encode_transaction(tx.to_dict()))
|
||||||
|
assert result.is_error()
|
Loading…
x
Reference in New Issue
Block a user