diff --git a/bigchaindb/backend/localmongodb/query.py b/bigchaindb/backend/localmongodb/query.py index 4c08827b..dbd072d5 100644 --- a/bigchaindb/backend/localmongodb/query.py +++ b/bigchaindb/backend/localmongodb/query.py @@ -111,3 +111,27 @@ def get_txids_filtered(conn, asset_id, operation=None): @register_query(LocalMongoDBConnection) def text_search(*args, **kwargs): return mongodb.query.text_search(*args, **kwargs) + + +@register_query(LocalMongoDBConnection) +def get_owned_ids(conn, owner): + cursor = conn.run( + conn.collection('transactions').aggregate([ + {'$match': {'outputs.public_keys': owner}}, + {'$project': {'_id': False}} + ])) + return cursor + + +@register_query(LocalMongoDBConnection) +def get_spending_transactions(conn, inputs): + cursor = conn.run( + conn.collection('transactions').aggregate([ + {'$match': { + 'inputs.fulfills': { + '$in': inputs, + }, + }}, + {'$project': {'_id': False}} + ])) + return cursor diff --git a/bigchaindb/tendermint/fastquery.py b/bigchaindb/tendermint/fastquery.py new file mode 100644 index 00000000..7da15b99 --- /dev/null +++ b/bigchaindb/tendermint/fastquery.py @@ -0,0 +1,48 @@ +from bigchaindb.utils import condition_details_has_owner +from bigchaindb.backend import query +from bigchaindb.common.transaction import TransactionLink + + +class FastQuery(): + """ + Database queries that join on block results from a single node. + """ + + def get_outputs_by_public_key(self, public_key): + """ + Get outputs for a public key + """ + txs = list(query.get_owned_ids(self.connection, public_key)) + return [TransactionLink(tx['id'], index) + for tx in txs + for index, output in enumerate(tx['outputs']) + if condition_details_has_owner(output['condition']['details'], + public_key)] + + def filter_spent_outputs(self, outputs): + """ + Remove outputs that have been spent + + Args: + outputs: list of TransactionLink + """ + links = [o.to_dict() for o in outputs] + txs = list(query.get_spending_transactions(self.connection, links)) + spends = {TransactionLink.from_dict(input_['fulfills']) + for tx in txs + for input_ in tx['inputs']} + return [ff for ff in outputs if ff not in spends] + + def filter_unspent_outputs(self, outputs): + """ + Remove outputs that have not been spent + + Args: + outputs: list of TransactionLink + """ + links = [o.to_dict() for o in outputs] + txs = list(query.get_spending_transactions(self.connection, links)) + spends = {TransactionLink.from_dict(input_['fulfills']) + for tx in txs + for input_ in tx['inputs']} + return [ff for ff in outputs if ff in spends] diff --git a/bigchaindb/tendermint/lib.py b/bigchaindb/tendermint/lib.py index 5b0f1bdb..67f899e4 100644 --- a/bigchaindb/tendermint/lib.py +++ b/bigchaindb/tendermint/lib.py @@ -11,6 +11,7 @@ from bigchaindb import Bigchain from bigchaindb.models import Transaction from bigchaindb.common.exceptions import SchemaValidationError, ValidationError from bigchaindb.tendermint.utils import encode_transaction +from bigchaindb.tendermint import fastquery logger = logging.getLogger(__name__) @@ -117,5 +118,9 @@ class BigchainDB(Bigchain): return False return transaction + @property + def fastquery(self): + return fastquery.FastQuery(self.connection, self.me) + Block = namedtuple('Block', ('app_hash', 'height')) diff --git a/tests/backend/localmongodb/test_queries.py b/tests/backend/localmongodb/test_queries.py index fc9672d8..5456a68f 100644 --- a/tests/backend/localmongodb/test_queries.py +++ b/tests/backend/localmongodb/test_queries.py @@ -3,10 +3,9 @@ from copy import deepcopy import pytest import pymongo -pytestmark = [pytest.mark.tendermint, pytest.mark.localmongodb] +pytestmark = [pytest.mark.tendermint, pytest.mark.localmongodb, pytest.mark.bdb] -@pytest.mark.bdb def test_get_txids_filtered(signed_create_tx, signed_transfer_tx): from bigchaindb.backend import connect, query from bigchaindb.models import Transaction @@ -32,7 +31,6 @@ def test_get_txids_filtered(signed_create_tx, signed_transfer_tx): assert txids == {signed_transfer_tx.id} -@pytest.mark.bdb def test_write_assets(): from bigchaindb.backend import connect, query conn = connect() @@ -57,7 +55,6 @@ def test_write_assets(): assert list(cursor) == assets[:-1] -@pytest.mark.bdb def test_get_assets(): from bigchaindb.backend import connect, query conn = connect() @@ -74,8 +71,40 @@ def test_get_assets(): assert query.get_asset(conn, asset['id']) -@pytest.mark.bdb def test_text_search(): from ..mongodb.test_queries import test_text_search test_text_search('assets') + + +def test_get_owned_ids(signed_create_tx, user_pk): + from bigchaindb.backend import connect, query + conn = connect() + + # insert a transaction + conn.db.transactions.insert_one(signed_create_tx.to_dict()) + + txns = list(query.get_owned_ids(conn, user_pk)) + + assert txns[0] == signed_create_tx.to_dict() + + +def test_get_spending_transactions(user_pk): + from bigchaindb.backend import connect, query + from bigchaindb.models import Transaction + conn = connect() + + out = [([user_pk], 1)] + tx1 = Transaction.create([user_pk], out * 3) + inputs = tx1.to_inputs() + tx2 = Transaction.transfer([inputs[0]], out, tx1.id) + tx3 = Transaction.transfer([inputs[1]], out, tx1.id) + tx4 = Transaction.transfer([inputs[2]], out, tx1.id) + txns = [tx.to_dict() for tx in [tx1, tx2, tx3, tx4]] + conn.db.transactions.insert_many(txns) + + links = [inputs[0].fulfills.to_dict(), inputs[2].fulfills.to_dict()] + txns = list(query.get_spending_transactions(conn, links)) + + # tx3 not a member because input 1 not asked for + assert txns == [tx2.to_dict(), tx4.to_dict()] diff --git a/tests/tendermint/test_core.py b/tests/tendermint/test_core.py index 80b5cbb4..40b0310e 100644 --- a/tests/tendermint/test_core.py +++ b/tests/tendermint/test_core.py @@ -92,7 +92,7 @@ def test_deliver_transfer_tx__double_spend_fails(b): carly = generate_key_pair() asset = { - "msg": "live long and prosper" + 'msg': 'live long and prosper' } tx = Transaction.create([alice.public_key], diff --git a/tests/web/conftest.py b/tests/web/conftest.py index 213e4b7e..efb0f81b 100644 --- a/tests/web/conftest.py +++ b/tests/web/conftest.py @@ -3,11 +3,10 @@ import pytest @pytest.fixture def app(request): - from bigchaindb import config from bigchaindb.web import server from bigchaindb.tendermint.lib import BigchainDB - if config['database']['backend'] == 'localmongodb': + if request.config.getoption('--database-backend') == 'localmongodb': app = server.create_app(debug=True, bigchaindb_factory=BigchainDB) else: app = server.create_app(debug=True) diff --git a/tests/web/test_outputs.py b/tests/web/test_outputs.py index 8ef90b73..2d5e663d 100644 --- a/tests/web/test_outputs.py +++ b/tests/web/test_outputs.py @@ -6,11 +6,12 @@ pytestmark = [pytest.mark.bdb, pytest.mark.usefixtures('inputs')] OUTPUTS_ENDPOINT = '/api/v1/outputs/' +@pytest.mark.tendermint def test_get_outputs_endpoint(client, user_pk): m = MagicMock() m.txid = 'a' m.output = 0 - with patch('bigchaindb.core.Bigchain.get_outputs_filtered') as gof: + with patch('bigchaindb.tendermint.lib.BigchainDB.get_outputs_filtered') as gof: gof.return_value = [m, m] res = client.get(OUTPUTS_ENDPOINT + '?public_key={}'.format(user_pk)) assert res.json == [ @@ -21,11 +22,12 @@ def test_get_outputs_endpoint(client, user_pk): gof.assert_called_once_with(user_pk, None) +@pytest.mark.tendermint def test_get_outputs_endpoint_unspent(client, user_pk): m = MagicMock() m.txid = 'a' m.output = 0 - with patch('bigchaindb.core.Bigchain.get_outputs_filtered') as gof: + with patch('bigchaindb.tendermint.lib.BigchainDB.get_outputs_filtered') as gof: gof.return_value = [m] params = '?spent=False&public_key={}'.format(user_pk) res = client.get(OUTPUTS_ENDPOINT + params) @@ -34,11 +36,12 @@ def test_get_outputs_endpoint_unspent(client, user_pk): gof.assert_called_once_with(user_pk, False) +@pytest.mark.tendermint def test_get_outputs_endpoint_spent(client, user_pk): m = MagicMock() m.txid = 'a' m.output = 0 - with patch('bigchaindb.core.Bigchain.get_outputs_filtered') as gof: + with patch('bigchaindb.tendermint.lib.BigchainDB.get_outputs_filtered') as gof: gof.return_value = [m] params = '?spent=true&public_key={}'.format(user_pk) res = client.get(OUTPUTS_ENDPOINT + params) @@ -47,11 +50,13 @@ def test_get_outputs_endpoint_spent(client, user_pk): gof.assert_called_once_with(user_pk, True) +@pytest.mark.tendermint def test_get_outputs_endpoint_without_public_key(client): res = client.get(OUTPUTS_ENDPOINT) assert res.status_code == 400 +@pytest.mark.tendermint def test_get_outputs_endpoint_with_invalid_public_key(client): expected = {'message': {'public_key': 'Invalid base58 ed25519 key'}} res = client.get(OUTPUTS_ENDPOINT + '?public_key=abc') @@ -59,6 +64,7 @@ def test_get_outputs_endpoint_with_invalid_public_key(client): assert res.status_code == 400 +@pytest.mark.tendermint def test_get_outputs_endpoint_with_invalid_spent(client, user_pk): expected = {'message': {'spent': 'Boolean value must be "true" or "false" (lowercase)'}} params = '?spent=tru&public_key={}'.format(user_pk)