diff --git a/CHANGELOG.md b/CHANGELOG.md index 258087a..332e106 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,11 @@ For reference, the possible headings are: * **Known Issues** * **Notes** +## [2.4.1] - 2023-11-04 +* **Removed** Fastquery class +* **Changed** UTXO space updated to resemble outputs +* **Changed** updated UTXO querying + ## [2.4.0] - 2023-29-03 * **Added** Zenroom script validation * **Changed** adjusted zenroom testing for new transaction script structure diff --git a/planetmint/backend/localmongodb/query.py b/planetmint/backend/localmongodb/query.py index 947cf3e..a2951c4 100644 --- a/planetmint/backend/localmongodb/query.py +++ b/planetmint/backend/localmongodb/query.py @@ -77,7 +77,7 @@ def get_assets(conn, asset_ids): @register_query(LocalMongoDBConnection) -def get_spent(conn, transaction_id, output): +def get_spending_transaction(conn, transaction_id, output): query = { "inputs": { "$elemMatch": {"$and": [{"fulfills.transaction_id": transaction_id}, {"fulfills.output_index": output}]} @@ -167,21 +167,6 @@ def delete_transactions(conn, txn_ids): conn.run(conn.collection("transactions").delete_many({"id": {"$in": txn_ids}})) -@register_query(LocalMongoDBConnection) -def store_unspent_outputs(conn, *unspent_outputs): - if unspent_outputs: - try: - return conn.run( - conn.collection("utxos").insert_many( - unspent_outputs, - ordered=False, - ) - ) - except DuplicateKeyError: - # TODO log warning at least - pass - - @register_query(LocalMongoDBConnection) def delete_unspent_outputs(conn, *unspent_outputs): if unspent_outputs: diff --git a/planetmint/backend/query.py b/planetmint/backend/query.py index a8284c2..a6faa18 100644 --- a/planetmint/backend/query.py +++ b/planetmint/backend/query.py @@ -133,7 +133,7 @@ def get_asset(connection, asset_id) -> Asset: @singledispatch -def get_spent(connection, transaction_id, condition_id): +def get_spending_transaction(connection, transaction_id, condition_id): """Check if a `txid` was already used as an input. A transaction can be used as an input for another transaction. Bigchain @@ -208,7 +208,7 @@ def get_block_with_transaction(connection, txid): @singledispatch -def store_transaction_outputs(connection, output: Output, index: int): +def store_transaction_outputs(connection, output: Output, index: int, table: str): """Store the transaction outputs. Args: @@ -264,13 +264,6 @@ def store_block(conn, block): raise NotImplementedError -@singledispatch -def store_unspent_outputs(connection, unspent_outputs): - """Store unspent outputs in ``utxo_set`` table.""" - - raise NotImplementedError - - @singledispatch def delete_unspent_outputs(connection, unspent_outputs): """Delete unspent outputs in ``utxo_set`` table.""" @@ -455,6 +448,12 @@ def get_outputs_by_tx_id(connection, tx_id: str) -> list[Output]: raise NotImplementedError +@singledispatch +def get_outputs_by_owner(connection, public_key: str, table: str) -> list[Output]: + """Retrieve an owners outputs by public key""" + raise NotImplementedError + + @singledispatch def get_metadata(conn, transaction_ids): """Retrieve metadata for a list of transactions by their ids""" diff --git a/planetmint/backend/tarantool/init.lua b/planetmint/backend/tarantool/init.lua index ad302f8..83832cc 100644 --- a/planetmint/backend/tarantool/init.lua +++ b/planetmint/backend/tarantool/init.lua @@ -171,9 +171,11 @@ function init() utxos = box.schema.create_space('utxos', { if_not_exists = true }) utxos:format({ { name = 'id', type = 'string' }, - { name = 'transaction_id', type = 'string' }, - { name = 'output_index', type = 'unsigned' }, - { name = 'utxo', type = 'map' } + { name = 'amount' , type = 'unsigned' }, + { name = 'public_keys', type = 'array' }, + { name = 'condition', type = 'map' }, + { name = 'output_index', type = 'number' }, + { name = 'transaction_id' , type = 'string' } }) utxos:create_index('id', { if_not_exists = true, @@ -189,7 +191,13 @@ function init() parts = { { field = 'transaction_id', type = 'string' }, { field = 'output_index', type = 'unsigned' } - }}) + } + }) + utxos:create_index('public_keys', { + if_not_exists = true, + unique = false, + parts = {{field = 'public_keys[*]', type = 'string' }} + }) -- Elections diff --git a/planetmint/backend/tarantool/sync_io/query.py b/planetmint/backend/tarantool/sync_io/query.py index 171e712..0a8359d 100644 --- a/planetmint/backend/tarantool/sync_io/query.py +++ b/planetmint/backend/tarantool/sync_io/query.py @@ -127,11 +127,12 @@ def get_transactions_by_metadata(connection, metadata: str, limit: int = 1000) - return get_complete_transactions_by_ids(connection, tx_ids) +@register_query(TarantoolDBConnection) @catch_db_exception -def store_transaction_outputs(connection, output: Output, index: int) -> str: +def store_transaction_outputs(connection, output: Output, index: int, table=TARANT_TABLE_OUTPUT) -> str: output_id = uuid4().hex connection.connect().insert( - TARANT_TABLE_OUTPUT, + table, ( output_id, int(output.amount), @@ -220,7 +221,9 @@ def get_assets(connection, assets_ids: list) -> list[Asset]: @register_query(TarantoolDBConnection) @catch_db_exception -def get_spent(connection, fullfil_transaction_id: str, fullfil_output_index: str) -> list[DbTransaction]: +def get_spending_transaction( + connection, fullfil_transaction_id: str, fullfil_output_index: str +) -> list[DbTransaction]: _inputs = ( connection.connect() .select( @@ -300,7 +303,7 @@ def get_spending_transactions(connection, inputs): _transactions = [] for inp in inputs: - _trans_list = get_spent( + _trans_list = get_spending_transaction( fullfil_transaction_id=inp["transaction_id"], fullfil_output_index=inp["output_index"], connection=connection, @@ -337,6 +340,9 @@ def delete_transactions(connection, txn_ids: list): _outputs = get_outputs_by_tx_id(connection, _id) for x in range(len(_outputs)): connection.connect().call("delete_output", (_outputs[x].id)) + connection.connect().delete( + TARANT_TABLE_UTXOS, (_id, _outputs[x].index), index="utxo_by_transaction_id_and_output_index" + ) for _id in txn_ids: connection.connect().delete(TARANT_TABLE_TRANSACTION, _id) connection.connect().delete(TARANT_TABLE_GOVERNANCE, _id) @@ -344,26 +350,7 @@ def delete_transactions(connection, txn_ids: list): @register_query(TarantoolDBConnection) @catch_db_exception -def store_unspent_outputs(connection, *unspent_outputs: list): - result = [] - if unspent_outputs: - for utxo in unspent_outputs: - try: - output = ( - connection.connect() - .insert(TARANT_TABLE_UTXOS, (uuid4().hex, utxo["transaction_id"], utxo["output_index"], utxo)) - .data - ) - result.append(output) - except Exception as e: - logger.info(f"Could not insert unspent output: {e}") - raise OperationDataInsertionError() - return result - - -@register_query(TarantoolDBConnection) -@catch_db_exception -def delete_unspent_outputs(connection, *unspent_outputs: list): +def delete_unspent_outputs(connection, unspent_outputs: list): result = [] if unspent_outputs: for utxo in unspent_outputs: @@ -383,8 +370,8 @@ def delete_unspent_outputs(connection, *unspent_outputs: list): @register_query(TarantoolDBConnection) @catch_db_exception def get_unspent_outputs(connection, query=None): # for now we don't have implementation for 'query'. - _utxos = connection.connect().select(TARANT_TABLE_UTXOS, []).data - return [utx[3] for utx in _utxos] + utxos = connection.connect().select(TARANT_TABLE_UTXOS, []).data + return [{"transaction_id": utxo[5], "output_index": utxo[4]} for utxo in utxos] @register_query(TarantoolDBConnection) @@ -522,3 +509,10 @@ def get_latest_abci_chain(connection) -> Union[dict, None]: return None _chain = sorted(_all_chains, key=itemgetter(1), reverse=True)[0] return {"chain_id": _chain[0], "height": _chain[1], "is_synced": _chain[2]} + + +@register_query(TarantoolDBConnection) +@catch_db_exception +def get_outputs_by_owner(connection, public_key: str, table=TARANT_TABLE_OUTPUT) -> list[Output]: + outputs = connection.connect().select(table, public_key, index="public_keys") + return [Output.from_tuple(output) for output in outputs] diff --git a/planetmint/model/dataaccessor.py b/planetmint/model/dataaccessor.py index a488f96..de80a17 100644 --- a/planetmint/model/dataaccessor.py +++ b/planetmint/model/dataaccessor.py @@ -1,5 +1,6 @@ import rapidjson from itertools import chain +from hashlib import sha3_256 from transactions import Transaction from transactions.common.exceptions import DoubleSpend @@ -8,10 +9,14 @@ from transactions.common.exceptions import InputDoesNotExist from planetmint import config_utils, backend from planetmint.const import GOVERNANCE_TRANSACTION_TYPES -from planetmint.model.fastquery import FastQuery -from planetmint.abci.utils import key_from_base64 +from planetmint.abci.utils import key_from_base64, merkleroot from planetmint.backend.connection import Connection -from planetmint.backend.tarantool.const import TARANT_TABLE_TRANSACTION, TARANT_TABLE_GOVERNANCE +from planetmint.backend.tarantool.const import ( + TARANT_TABLE_TRANSACTION, + TARANT_TABLE_GOVERNANCE, + TARANT_TABLE_UTXOS, + TARANT_TABLE_OUTPUT, +) from planetmint.backend.models.block import Block from planetmint.backend.models.output import Output from planetmint.backend.models.asset import Asset @@ -37,6 +42,7 @@ class DataAccessor: backend.query.store_transactions(self.connection, txns, TARANT_TABLE_TRANSACTION) backend.query.store_transactions(self.connection, gov_txns, TARANT_TABLE_GOVERNANCE) + [self.update_utxoset(t) for t in txns + gov_txns] def delete_transactions(self, txs): return backend.query.delete_transactions(self.connection, txs) @@ -60,7 +66,7 @@ class DataAccessor: def get_outputs_by_tx_id(self, txid): return backend.query.get_outputs_by_tx_id(self.connection, txid) - def get_outputs_filtered(self, owner, spent=None): + def get_outputs_filtered(self, owner, spent=None) -> list[Output]: """Get a list of output links filtered on some criteria Args: @@ -70,16 +76,23 @@ class DataAccessor: not specified (``None``) return all outputs. Returns: - :obj:`list` of TransactionLink: list of ``txid`` s and ``output`` s + :obj:`list` of Output: list of ``txid`` s and ``output`` s pointing to another transaction's condition """ - outputs = self.fastquery.get_outputs_by_public_key(owner) - if spent is None: - return outputs - elif spent is True: - return self.fastquery.filter_unspent_outputs(outputs) + outputs = backend.query.get_outputs_by_owner(self.connection, owner) + unspent_outputs = backend.query.get_outputs_by_owner(self.connection, owner, TARANT_TABLE_UTXOS) + if spent is True: + spent_outputs = [] + for output in outputs: + if not any( + utxo.transaction_id == output.transaction_id and utxo.index == output.index + for utxo in unspent_outputs + ): + spent_outputs.append(output) + return spent_outputs elif spent is False: - return self.fastquery.filter_spent_outputs(outputs) + return unspent_outputs + return outputs def store_block(self, block): """Create a new block.""" @@ -138,8 +151,8 @@ class DataAccessor: return validators - def get_spent(self, txid, output, current_transactions=[]) -> DbTransaction: - transactions = backend.query.get_spent(self.connection, txid, output) + def get_spending_transaction(self, txid, output, current_transactions=[]) -> DbTransaction: + transactions = backend.query.get_spending_transaction(self.connection, txid, output) current_spent_transactions = [] for ctxn in current_transactions: @@ -196,7 +209,7 @@ class DataAccessor: if input_tx is None: raise InputDoesNotExist("input `{}` doesn't exist".format(input_txid)) - spent = self.get_spent(input_txid, input_.fulfills.output, current_transactions) + spent = self.get_spending_transaction(input_txid, input_.fulfills.output, current_transactions) if spent: raise DoubleSpend("input `{}` was already spent".format(input_txid)) @@ -277,6 +290,54 @@ class DataAccessor: txns = backend.query.get_asset_tokens_for_public_key(self.connection, transaction_id, election_pk) return txns - @property - def fastquery(self): - return FastQuery(self.connection) + def update_utxoset(self, transaction): + spent_outputs = [ + {"output_index": input["fulfills"]["output_index"], "transaction_id": input["fulfills"]["transaction_id"]} + for input in transaction["inputs"] + if input["fulfills"] != None + ] + + if spent_outputs: + backend.query.delete_unspent_outputs(self.connection, spent_outputs) + [ + backend.query.store_transaction_outputs( + self.connection, Output.outputs_dict(output, transaction["id"]), index, TARANT_TABLE_UTXOS + ) + for index, output in enumerate(transaction["outputs"]) + ] + + def get_utxoset_merkle_root(self): + """Returns the merkle root of the utxoset. This implies that + the utxoset is first put into a merkle tree. + + For now, the merkle tree and its root will be computed each + time. This obviously is not efficient and a better approach + that limits the repetition of the same computation when + unnecesary should be sought. For instance, future optimizations + could simply re-compute the branches of the tree that were + affected by a change. + + The transaction hash (id) and output index should be sufficient + to uniquely identify a utxo, and consequently only that + information from a utxo record is needed to compute the merkle + root. Hence, each node of the merkle tree should contain the + tuple (txid, output_index). + + .. important:: The leaves of the tree will need to be sorted in + some kind of lexicographical order. + + Returns: + str: Merkle root in hexadecimal form. + """ + utxoset = backend.query.get_unspent_outputs(self.connection) + # TODO Once ready, use the already pre-computed utxo_hash field. + # See common/transactions.py for details. + + hashes = [ + sha3_256("{}{}".format(utxo["transaction_id"], utxo["output_index"]).encode()).digest() for utxo in utxoset + ] + + print(sorted(hashes)) + + # TODO Notice the sorted call! + return merkleroot(sorted(hashes)) diff --git a/planetmint/model/fastquery.py b/planetmint/model/fastquery.py deleted file mode 100644 index b8ef775..0000000 --- a/planetmint/model/fastquery.py +++ /dev/null @@ -1,76 +0,0 @@ -# Copyright © 2020 Interplanetary Database Association e.V., -# Planetmint and IPDB software contributors. -# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) -# Code is Apache-2.0 and docs are CC-BY-4.0 - -from planetmint.backend import query -from transactions.common.transaction import TransactionLink - -from planetmint.backend.models.output import ConditionDetails - - -class FastQuery: - """Database queries that join on block results from a single node.""" - - def __init__(self, connection): - self.connection = connection - - def get_outputs_by_public_key(self, public_key): - """Get outputs for a public key""" - txs = 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 = query.get_spending_transactions(self.connection, links) - spends = {TransactionLink.from_dict(input.fulfills.to_dict()) 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 = query.get_spending_transactions(self.connection, links) - spends = {TransactionLink.from_dict(input.fulfills.to_dict()) for tx in txs for input in tx.inputs} - return [ff for ff in outputs if ff in spends] - - -# TODO: Rename this function, it's handling fulfillments not conditions -def condition_details_has_owner(condition_details, owner): - """Check if the public_key of owner is in the condition details - as an Ed25519Fulfillment.public_key - - Args: - condition_details (dict): dict with condition details - owner (str): base58 public key of owner - - Returns: - bool: True if the public key is found in the condition details, False otherwise - - """ - if isinstance(condition_details, ConditionDetails) and condition_details.sub_conditions is not None: - result = condition_details_has_owner(condition_details.sub_conditions, owner) - if result: - return True - elif isinstance(condition_details, list): - for subcondition in condition_details: - result = condition_details_has_owner(subcondition, owner) - if result: - return True - else: - if condition_details.public_key is not None and owner == condition_details.public_key: - return True - return False diff --git a/pyproject.toml b/pyproject.toml index 38e67e5..e20f8a8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "planetmint" -version = "2.4.0" +version = "2.4.1" description = "Planetmint: The Blockchain Database" authors = ["Planetmint contributors"] license = "AGPLv3" diff --git a/tests/backend/test_generics.py b/tests/backend/test_generics.py index 0613fd8..37abf47 100644 --- a/tests/backend/test_generics.py +++ b/tests/backend/test_generics.py @@ -29,7 +29,7 @@ def test_schema(schema_func_name, args_qty): ("get_txids_filtered", 1), ("get_owned_ids", 1), ("get_block", 1), - ("get_spent", 2), + ("get_spending_transaction", 2), ("get_spending_transactions", 1), ("store_assets", 1), ("get_asset", 1), diff --git a/tests/conftest.py b/tests/conftest.py index 9f99c3e..db20c8c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -484,50 +484,6 @@ def wsserver_base_url(wsserver_scheme, wsserver_host, wsserver_port): return "{}://{}:{}".format(wsserver_scheme, wsserver_host, wsserver_port) -@pytest.fixture -def unspent_output_0(): - return { - "amount": 1, - "asset_id": "e897c7a0426461a02b4fca8ed73bc0debed7570cf3b40fb4f49c963434225a4d", - "condition_uri": "ni:///sha-256;RmovleG60-7K0CX60jjfUunV3lBpUOkiQOAnBzghm0w?fpt=ed25519-sha-256&cost=131072", - "fulfillment_message": '{"asset":{"data":{"hash":"06e47bcf9084f7ecfd2a2a2ad275444a"}},"id":"e897c7a0426461a02b4fca8ed73bc0debed7570cf3b40fb4f49c963434225a4d","inputs":[{"fulfillment":"pGSAIIQT0Jm6LDlcSs9coJK4Q4W-SNtsO2EtMtQJ04EUjBMJgUAXKIqeaippbF-IClhhZNNaP6EIZ_OgrVQYU4mH6b-Vc3Tg-k6p-rJOlLGUUo_w8C5QgPHNRYFOqUk2f1q0Cs4G","fulfills":null,"owners_before":["9taLkHkaBXeSF8vrhDGFTAmcZuCEPqjQrKadfYGs4gHv"]}],"metadata":null,"operation":"CREATE","outputs":[{"amount":"1","condition":{"details":{"public_key":"6FDGsHrR9RZqNaEm7kBvqtxRkrvuWogBW2Uy7BkWc5Tz","type":"ed25519-sha-256"},"uri":"ni:///sha-256;RmovleG60-7K0CX60jjfUunV3lBpUOkiQOAnBzghm0w?fpt=ed25519-sha-256&cost=131072"},"public_keys":["6FDGsHrR9RZqNaEm7kBvqtxRkrvuWogBW2Uy7BkWc5Tz"]},{"amount":"2","condition":{"details":{"public_key":"AH9D7xgmhyLmVE944zvHvuvYWuj5DfbMBJhnDM4A5FdT","type":"ed25519-sha-256"},"uri":"ni:///sha-256;-HlYmgwwl-vXwE52IaADhvYxaL1TbjqfJ-LGn5a1PFc?fpt=ed25519-sha-256&cost=131072"},"public_keys":["AH9D7xgmhyLmVE944zvHvuvYWuj5DfbMBJhnDM4A5FdT"]},{"amount":"3","condition":{"details":{"public_key":"HpmSVrojHvfCXQbmoAs4v6Aq1oZiZsZDnjr68KiVtPbB","type":"ed25519-sha-256"},"uri":"ni:///sha-256;xfn8pvQkTCPtvR0trpHy2pqkkNTmMBCjWMMOHtk3WO4?fpt=ed25519-sha-256&cost=131072"},"public_keys":["HpmSVrojHvfCXQbmoAs4v6Aq1oZiZsZDnjr68KiVtPbB"]}],"version":"1.0"}', # noqa: E501 - # noqa - "output_index": 0, - "transaction_id": "e897c7a0426461a02b4fca8ed73bc0debed7570cf3b40fb4f49c963434225a4d", - } - - -@pytest.fixture -def unspent_output_1(): - return { - "amount": 2, - "asset_id": "e897c7a0426461a02b4fca8ed73bc0debed7570cf3b40fb4f49c963434225a4d", - "condition_uri": "ni:///sha-256;-HlYmgwwl-vXwE52IaADhvYxaL1TbjqfJ-LGn5a1PFc?fpt=ed25519-sha-256&cost=131072", - "fulfillment_message": '{"asset":{"data":{"hash":"06e47bcf9084f7ecfd2a2a2ad275444a"}},"id":"e897c7a0426461a02b4fca8ed73bc0debed7570cf3b40fb4f49c963434225a4d","inputs":[{"fulfillment":"pGSAIIQT0Jm6LDlcSs9coJK4Q4W-SNtsO2EtMtQJ04EUjBMJgUAXKIqeaippbF-IClhhZNNaP6EIZ_OgrVQYU4mH6b-Vc3Tg-k6p-rJOlLGUUo_w8C5QgPHNRYFOqUk2f1q0Cs4G","fulfills":null,"owners_before":["9taLkHkaBXeSF8vrhDGFTAmcZuCEPqjQrKadfYGs4gHv"]}],"metadata":null,"operation":"CREATE","outputs":[{"amount":"1","condition":{"details":{"public_key":"6FDGsHrR9RZqNaEm7kBvqtxRkrvuWogBW2Uy7BkWc5Tz","type":"ed25519-sha-256"},"uri":"ni:///sha-256;RmovleG60-7K0CX60jjfUunV3lBpUOkiQOAnBzghm0w?fpt=ed25519-sha-256&cost=131072"},"public_keys":["6FDGsHrR9RZqNaEm7kBvqtxRkrvuWogBW2Uy7BkWc5Tz"]},{"amount":"2","condition":{"details":{"public_key":"AH9D7xgmhyLmVE944zvHvuvYWuj5DfbMBJhnDM4A5FdT","type":"ed25519-sha-256"},"uri":"ni:///sha-256;-HlYmgwwl-vXwE52IaADhvYxaL1TbjqfJ-LGn5a1PFc?fpt=ed25519-sha-256&cost=131072"},"public_keys":["AH9D7xgmhyLmVE944zvHvuvYWuj5DfbMBJhnDM4A5FdT"]},{"amount":"3","condition":{"details":{"public_key":"HpmSVrojHvfCXQbmoAs4v6Aq1oZiZsZDnjr68KiVtPbB","type":"ed25519-sha-256"},"uri":"ni:///sha-256;xfn8pvQkTCPtvR0trpHy2pqkkNTmMBCjWMMOHtk3WO4?fpt=ed25519-sha-256&cost=131072"},"public_keys":["HpmSVrojHvfCXQbmoAs4v6Aq1oZiZsZDnjr68KiVtPbB"]}],"version":"1.0"}', # noqa: E501 - # noqa - "output_index": 1, - "transaction_id": "e897c7a0426461a02b4fca8ed73bc0debed7570cf3b40fb4f49c963434225a4d", - } - - -@pytest.fixture -def unspent_output_2(): - return { - "amount": 3, - "asset_id": "e897c7a0426461a02b4fca8ed73bc0debed7570cf3b40fb4f49c963434225a4d", - "condition_uri": "ni:///sha-256;xfn8pvQkTCPtvR0trpHy2pqkkNTmMBCjWMMOHtk3WO4?fpt=ed25519-sha-256&cost=131072", - "fulfillment_message": '{"asset":{"data":{"hash":"06e47bcf9084f7ecfd2a2a2ad275444a"}},"id":"e897c7a0426461a02b4fca8ed73bc0debed7570cf3b40fb4f49c963434225a4d","inputs":[{"fulfillment":"pGSAIIQT0Jm6LDlcSs9coJK4Q4W-SNtsO2EtMtQJ04EUjBMJgUAXKIqeaippbF-IClhhZNNaP6EIZ_OgrVQYU4mH6b-Vc3Tg-k6p-rJOlLGUUo_w8C5QgPHNRYFOqUk2f1q0Cs4G","fulfills":null,"owners_before":["9taLkHkaBXeSF8vrhDGFTAmcZuCEPqjQrKadfYGs4gHv"]}],"metadata":null,"operation":"CREATE","outputs":[{"amount":"1","condition":{"details":{"public_key":"6FDGsHrR9RZqNaEm7kBvqtxRkrvuWogBW2Uy7BkWc5Tz","type":"ed25519-sha-256"},"uri":"ni:///sha-256;RmovleG60-7K0CX60jjfUunV3lBpUOkiQOAnBzghm0w?fpt=ed25519-sha-256&cost=131072"},"public_keys":["6FDGsHrR9RZqNaEm7kBvqtxRkrvuWogBW2Uy7BkWc5Tz"]},{"amount":"2","condition":{"details":{"public_key":"AH9D7xgmhyLmVE944zvHvuvYWuj5DfbMBJhnDM4A5FdT","type":"ed25519-sha-256"},"uri":"ni:///sha-256;-HlYmgwwl-vXwE52IaADhvYxaL1TbjqfJ-LGn5a1PFc?fpt=ed25519-sha-256&cost=131072"},"public_keys":["AH9D7xgmhyLmVE944zvHvuvYWuj5DfbMBJhnDM4A5FdT"]},{"amount":"3","condition":{"details":{"public_key":"HpmSVrojHvfCXQbmoAs4v6Aq1oZiZsZDnjr68KiVtPbB","type":"ed25519-sha-256"},"uri":"ni:///sha-256;xfn8pvQkTCPtvR0trpHy2pqkkNTmMBCjWMMOHtk3WO4?fpt=ed25519-sha-256&cost=131072"},"public_keys":["HpmSVrojHvfCXQbmoAs4v6Aq1oZiZsZDnjr68KiVtPbB"]}],"version":"1.0"}', # noqa: E501 - # noqa - "output_index": 2, - "transaction_id": "e897c7a0426461a02b4fca8ed73bc0debed7570cf3b40fb4f49c963434225a4d", - } - - -@pytest.fixture -def unspent_outputs(unspent_output_0, unspent_output_1, unspent_output_2): - return unspent_output_0, unspent_output_1, unspent_output_2 - - @pytest.fixture def tarantool_client(db_context): # TODO Here add TarantoolConnectionClass return TarantoolDBConnection(host=db_context.host, port=db_context.port) @@ -538,28 +494,6 @@ def utxo_collection(tarantool_client, _setup_database): return tarantool_client.get_space("utxos") -@pytest.fixture -def dummy_unspent_outputs(): - return [ - {"transaction_id": "a", "output_index": 0}, - {"transaction_id": "a", "output_index": 1}, - {"transaction_id": "b", "output_index": 0}, - ] - - -@pytest.fixture -def utxoset(dummy_unspent_outputs, utxo_collection): - from uuid import uuid4 - - num_rows_before_operation = utxo_collection.select().rowcount - for utxo in dummy_unspent_outputs: - res = utxo_collection.insert((uuid4().hex, utxo["transaction_id"], utxo["output_index"], utxo)) - assert res - num_rows_after_operation = utxo_collection.select().rowcount - assert num_rows_after_operation == num_rows_before_operation + 3 - return dummy_unspent_outputs, utxo_collection - - @pytest.fixture def network_validators(node_keys): validator_pub_power = {} diff --git a/tests/db/test_planetmint_api.py b/tests/db/test_planetmint_api.py index 1fac008..ea44eda 100644 --- a/tests/db/test_planetmint_api.py +++ b/tests/db/test_planetmint_api.py @@ -7,10 +7,10 @@ from unittest.mock import patch import pytest from base58 import b58decode from ipld import marshal, multihash +from operator import attrgetter from transactions.common import crypto -from transactions.common.transaction import TransactionLink -from transactions.common.transaction import Transaction +from transactions.common.transaction import Transaction, TransactionLink, Input from transactions.types.assets.create import Create from transactions.types.assets.transfer import Transfer from planetmint.exceptions import CriticalDoubleSpend @@ -64,7 +64,6 @@ class TestBigchainApi(object): def test_non_create_input_not_found(self, b, user_pk): from planetmint_cryptoconditions import Ed25519Sha256 from transactions.common.exceptions import InputDoesNotExist - from transactions.common.transaction import Input, TransactionLink # Create an input for a non existing transaction input = Input( @@ -104,14 +103,15 @@ class TestTransactionValidation(object): def test_non_create_valid_input_wrong_owner(self, b, user_pk): from transactions.common.crypto import generate_key_pair from transactions.common.exceptions import InvalidSignature + from transactions.common.transaction_link import TransactionLink - input_tx = b.models.fastquery.get_outputs_by_public_key(user_pk).pop() - input_transaction = b.models.get_transaction(input_tx.txid) + output = b.models.get_outputs_filtered(user_pk).pop() + input_transaction = b.models.get_transaction(output.transaction_id) sk, pk = generate_key_pair() tx = Create.generate([pk], [([user_pk], 1)]) tx.operation = "TRANSFER" tx.assets = [{"id": input_transaction.id}] - tx.inputs[0].fulfills = input_tx + tx.inputs[0].fulfills = TransactionLink(output.transaction_id, output.index) with pytest.raises(InvalidSignature): b.validate_transaction(tx) @@ -129,8 +129,8 @@ class TestTransactionValidation(object): class TestMultipleInputs(object): def test_transfer_single_owner_single_input(self, b, inputs, user_pk, user_sk): user2_sk, user2_pk = crypto.generate_key_pair() - tx_link = b.models.fastquery.get_outputs_by_public_key(user_pk).pop() - input_tx = b.models.get_transaction(tx_link.txid) + tx_output = b.models.get_outputs_filtered(user_pk).pop() + input_tx = b.models.get_transaction(tx_output.transaction_id) tx_converted = Transaction.from_dict(input_tx.to_dict(), True) tx = Transfer.generate(tx_converted.to_inputs(), [([user2_pk], 1)], asset_ids=[input_tx.id]) @@ -144,9 +144,9 @@ class TestMultipleInputs(object): def test_single_owner_before_multiple_owners_after_single_input(self, b, user_sk, user_pk, inputs): user2_sk, user2_pk = crypto.generate_key_pair() user3_sk, user3_pk = crypto.generate_key_pair() - tx_link = b.models.fastquery.get_outputs_by_public_key(user_pk).pop() + tx_output = b.models.get_outputs_filtered(user_pk).pop() - input_tx = b.models.get_transaction(tx_link.txid) + input_tx = b.models.get_transaction(tx_output.transaction_id) tx_converted = Transaction.from_dict(input_tx.to_dict(), True) tx = Transfer.generate(tx_converted.to_inputs(), [([user2_pk, user3_pk], 1)], asset_ids=[input_tx.id]) @@ -165,8 +165,8 @@ class TestMultipleInputs(object): tx = tx.sign([alice.private_key]) b.models.store_bulk_transactions([tx]) - owned_input = b.models.fastquery.get_outputs_by_public_key(user_pk).pop() - input_tx = b.models.get_transaction(owned_input.txid) + tx_output = b.models.get_outputs_filtered(user_pk).pop() + input_tx = b.models.get_transaction(tx_output.transaction_id) input_tx_converted = Transaction.from_dict(input_tx.to_dict(), True) transfer_tx = Transfer.generate(input_tx_converted.to_inputs(), [([user3_pk], 1)], asset_ids=[input_tx.id]) @@ -188,8 +188,8 @@ class TestMultipleInputs(object): b.models.store_bulk_transactions([tx]) # get input - tx_link = b.models.fastquery.get_outputs_by_public_key(user_pk).pop() - tx_input = b.models.get_transaction(tx_link.txid) + tx_output = b.models.get_outputs_filtered(user_pk).pop() + tx_input = b.models.get_transaction(tx_output.transaction_id) input_tx_converted = Transaction.from_dict(tx_input.to_dict(), True) tx = Transfer.generate(input_tx_converted.to_inputs(), [([user3_pk, user4_pk], 1)], asset_ids=[tx_input.id]) @@ -206,20 +206,24 @@ class TestMultipleInputs(object): tx = tx.sign([alice.private_key]) b.models.store_bulk_transactions([tx]) - owned_inputs_user1 = b.models.fastquery.get_outputs_by_public_key(user_pk) - owned_inputs_user2 = b.models.fastquery.get_outputs_by_public_key(user2_pk) - assert owned_inputs_user1 == [TransactionLink(tx.id, 0)] + stored_tx = b.models.get_transaction(tx.id) + + owned_inputs_user1 = b.models.get_outputs_filtered(user_pk) + owned_inputs_user2 = b.models.get_outputs_filtered(user2_pk) + assert owned_inputs_user1 == [stored_tx.outputs[0]] assert owned_inputs_user2 == [] tx_transfer = Transfer.generate(tx.to_inputs(), [([user2_pk], 1)], asset_ids=[tx.id]) tx_transfer = tx_transfer.sign([user_sk]) b.models.store_bulk_transactions([tx_transfer]) - owned_inputs_user1 = b.models.fastquery.get_outputs_by_public_key(user_pk) - owned_inputs_user2 = b.models.fastquery.get_outputs_by_public_key(user2_pk) + owned_inputs_user1 = b.models.get_outputs_filtered(user_pk) + owned_inputs_user2 = b.models.get_outputs_filtered(user2_pk) - assert owned_inputs_user1 == [TransactionLink(tx.id, 0)] - assert owned_inputs_user2 == [TransactionLink(tx_transfer.id, 0)] + stored_tx_transfer = b.models.get_transaction(tx_transfer.id) + + assert owned_inputs_user1 == [stored_tx.outputs[0]] + assert owned_inputs_user2 == [stored_tx_transfer.outputs[0]] def test_get_owned_ids_single_tx_multiple_outputs(self, b, user_sk, user_pk, alice): user2_sk, user2_pk = crypto.generate_key_pair() @@ -230,11 +234,15 @@ class TestMultipleInputs(object): b.models.store_bulk_transactions([tx_create_signed]) # get input - owned_inputs_user1 = b.models.fastquery.get_outputs_by_public_key(user_pk) - owned_inputs_user2 = b.models.fastquery.get_outputs_by_public_key(user2_pk) + owned_inputs_user1 = b.models.get_outputs_filtered(user_pk) + owned_inputs_user2 = b.models.get_outputs_filtered(user2_pk) - expected_owned_inputs_user1 = [TransactionLink(tx_create.id, 0), TransactionLink(tx_create.id, 1)] - assert owned_inputs_user1 == expected_owned_inputs_user1 + stored_tx = b.models.get_transaction(tx_create.id) + + expected_owned_inputs_user1 = [stored_tx.outputs[0], stored_tx.outputs[1]] + assert sorted(owned_inputs_user1, key=attrgetter("index")) == sorted( + expected_owned_inputs_user1, key=attrgetter("index") + ) assert owned_inputs_user2 == [] # transfer divisible asset divided in two outputs @@ -243,11 +251,16 @@ class TestMultipleInputs(object): ) tx_transfer_signed = tx_transfer.sign([user_sk]) b.models.store_bulk_transactions([tx_transfer_signed]) + stored_tx_transfer = b.models.get_transaction(tx_transfer.id) - owned_inputs_user1 = b.models.fastquery.get_outputs_by_public_key(user_pk) - owned_inputs_user2 = b.models.fastquery.get_outputs_by_public_key(user2_pk) - assert owned_inputs_user1 == expected_owned_inputs_user1 - assert owned_inputs_user2 == [TransactionLink(tx_transfer.id, 0), TransactionLink(tx_transfer.id, 1)] + owned_inputs_user1 = b.models.get_outputs_filtered(user_pk) + owned_inputs_user2 = b.models.get_outputs_filtered(user2_pk) + assert sorted(owned_inputs_user1, key=attrgetter("index")) == sorted( + expected_owned_inputs_user1, key=attrgetter("index") + ) + assert sorted(owned_inputs_user2, key=attrgetter("index")) == sorted( + [stored_tx_transfer.outputs[0], stored_tx_transfer.outputs[1]], key=attrgetter("index") + ) def test_get_owned_ids_multiple_owners(self, b, user_sk, user_pk, alice): user2_sk, user2_pk = crypto.generate_key_pair() @@ -257,10 +270,11 @@ class TestMultipleInputs(object): tx = tx.sign([alice.private_key]) b.models.store_bulk_transactions([tx]) + stored_tx = b.models.get_transaction(tx.id) - owned_inputs_user1 = b.models.fastquery.get_outputs_by_public_key(user_pk) - owned_inputs_user2 = b.models.fastquery.get_outputs_by_public_key(user_pk) - expected_owned_inputs_user1 = [TransactionLink(tx.id, 0)] + owned_inputs_user1 = b.models.get_outputs_filtered(user_pk) + owned_inputs_user2 = b.models.get_outputs_filtered(user_pk) + expected_owned_inputs_user1 = [stored_tx.outputs[0]] assert owned_inputs_user1 == owned_inputs_user2 assert owned_inputs_user1 == expected_owned_inputs_user1 @@ -269,9 +283,9 @@ class TestMultipleInputs(object): tx = tx.sign([user_sk, user2_sk]) b.models.store_bulk_transactions([tx]) - owned_inputs_user1 = b.models.fastquery.get_outputs_by_public_key(user_pk) - owned_inputs_user2 = b.models.fastquery.get_outputs_by_public_key(user2_pk) - spent_user1 = b.models.get_spent(tx.id, 0) + owned_inputs_user1 = b.models.get_outputs_filtered(user_pk) + owned_inputs_user2 = b.models.get_outputs_filtered(user2_pk) + spent_user1 = b.models.get_spending_transaction(tx.id, 0) assert owned_inputs_user1 == owned_inputs_user2 assert not spent_user1 @@ -283,11 +297,11 @@ class TestMultipleInputs(object): tx = tx.sign([alice.private_key]) b.models.store_bulk_transactions([tx]) - owned_inputs_user1 = b.models.fastquery.get_outputs_by_public_key(user_pk).pop() + owned_inputs_user1 = b.models.get_outputs_filtered(user_pk).pop() # check spents - input_txid = owned_inputs_user1.txid - spent_inputs_user1 = b.models.get_spent(input_txid, 0) + input_txid = owned_inputs_user1.transaction_id + spent_inputs_user1 = b.models.get_spending_transaction(input_txid, 0) assert spent_inputs_user1 is None # create a transaction and send it @@ -295,7 +309,7 @@ class TestMultipleInputs(object): tx = tx.sign([user_sk]) b.models.store_bulk_transactions([tx]) - spent_inputs_user1 = b.models.get_spent(input_txid, 0) + spent_inputs_user1 = b.models.get_spending_transaction(input_txid, 0) assert spent_inputs_user1 == tx.to_dict() def test_get_spent_single_tx_multiple_outputs(self, b, user_sk, user_pk, alice): @@ -307,11 +321,11 @@ class TestMultipleInputs(object): tx_create_signed = tx_create.sign([alice.private_key]) b.models.store_bulk_transactions([tx_create_signed]) - owned_inputs_user1 = b.models.fastquery.get_outputs_by_public_key(user_pk) + owned_inputs_user1 = b.models.get_outputs_filtered(user_pk) # check spents for input_tx in owned_inputs_user1: - assert b.models.get_spent(input_tx.txid, input_tx.output) is None + assert b.models.get_spending_transaction(input_tx.transaction_id, input_tx.index) is None # transfer the first 2 inputs tx_transfer = Transfer.generate( @@ -322,12 +336,12 @@ class TestMultipleInputs(object): # check that used inputs are marked as spent for ffill in tx_create.to_inputs()[:2]: - spent_tx = b.models.get_spent(ffill.fulfills.txid, ffill.fulfills.output) + spent_tx = b.models.get_spending_transaction(ffill.fulfills.txid, ffill.fulfills.output) assert spent_tx == tx_transfer_signed.to_dict() # check if remaining transaction that was unspent is also perceived # spendable by Planetmint - assert b.models.get_spent(tx_create.to_inputs()[2].fulfills.txid, 2) is None + assert b.models.get_spending_transaction(tx_create.to_inputs()[2].fulfills.txid, 2) is None def test_get_spent_multiple_owners(self, b, user_sk, user_pk, alice): user2_sk, user2_pk = crypto.generate_key_pair() @@ -342,10 +356,10 @@ class TestMultipleInputs(object): b.models.store_bulk_transactions(transactions) - owned_inputs_user1 = b.models.fastquery.get_outputs_by_public_key(user_pk) + owned_inputs_user1 = b.models.get_outputs_filtered(user_pk) # check spents for input_tx in owned_inputs_user1: - assert b.models.get_spent(input_tx.txid, input_tx.output) is None + assert b.models.get_spending_transaction(input_tx.transaction_id, input_tx.index) is None # create a transaction tx = Transfer.generate(transactions[0].to_inputs(), [([user3_pk], 1)], asset_ids=[transactions[0].id]) @@ -353,59 +367,49 @@ class TestMultipleInputs(object): b.models.store_bulk_transactions([tx]) # check that used inputs are marked as spent - assert b.models.get_spent(transactions[0].id, 0) == tx.to_dict() + assert b.models.get_spending_transaction(transactions[0].id, 0) == tx.to_dict() # check that the other remain marked as unspent for unspent in transactions[1:]: - assert b.models.get_spent(unspent.id, 0) is None + assert b.models.get_spending_transaction(unspent.id, 0) is None -def test_get_outputs_filtered_only_unspent(b): - from transactions.common.transaction import TransactionLink +def test_get_outputs_filtered_only_unspent(b, alice): + tx = Create.generate([alice.public_key], [([alice.public_key], 1), ([alice.public_key], 1)]) + tx = tx.sign([alice.private_key]) + b.models.store_bulk_transactions([tx]) - go = "planetmint.model.fastquery.FastQuery.get_outputs_by_public_key" - with patch(go) as get_outputs: - get_outputs.return_value = [TransactionLink("a", 1), TransactionLink("b", 2)] - fs = "planetmint.model.fastquery.FastQuery.filter_spent_outputs" - with patch(fs) as filter_spent: - filter_spent.return_value = [TransactionLink("b", 2)] - out = b.models.get_outputs_filtered("abc", spent=False) - get_outputs.assert_called_once_with("abc") - assert out == [TransactionLink("b", 2)] + tx_transfer = Transfer.generate(tx.to_inputs([0]), [([alice.public_key], 1)], asset_ids=[tx.id]) + tx_transfer = tx_transfer.sign([alice.private_key]) + b.models.store_bulk_transactions([tx_transfer]) + + outputs = b.models.get_outputs_filtered(alice.public_key, spent=False) + assert len(outputs) == 2 -def test_get_outputs_filtered_only_spent(b): - from transactions.common.transaction import TransactionLink +def test_get_outputs_filtered_only_spent(b, alice): + tx = Create.generate([alice.public_key], [([alice.public_key], 1), ([alice.public_key], 1)]) + tx = tx.sign([alice.private_key]) + b.models.store_bulk_transactions([tx]) - go = "planetmint.model.fastquery.FastQuery.get_outputs_by_public_key" - with patch(go) as get_outputs: - get_outputs.return_value = [TransactionLink("a", 1), TransactionLink("b", 2)] - fs = "planetmint.model.fastquery.FastQuery.filter_unspent_outputs" - with patch(fs) as filter_spent: - filter_spent.return_value = [TransactionLink("b", 2)] - out = b.models.get_outputs_filtered("abc", spent=True) - get_outputs.assert_called_once_with("abc") - assert out == [TransactionLink("b", 2)] + tx_transfer = Transfer.generate(tx.to_inputs([0]), [([alice.public_key], 1)], asset_ids=[tx.id]) + tx_transfer = tx_transfer.sign([alice.private_key]) + b.models.store_bulk_transactions([tx_transfer]) + + outputs = b.models.get_outputs_filtered(alice.public_key, spent=True) + assert len(outputs) == 1 -# @patch("planetmint.model.fastquery.FastQuery.filter_unspent_outputs") -# @patch("planetmint.model.fastquery.FastQuery.filter_spent_outputs") -def test_get_outputs_filtered( - b, - mocker, -): - from transactions.common.transaction import TransactionLink +def test_get_outputs_filtered(b, alice): + tx = Create.generate([alice.public_key], [([alice.public_key], 1), ([alice.public_key], 1)]) + tx = tx.sign([alice.private_key]) + b.models.store_bulk_transactions([tx]) - mock_filter_spent_outputs = mocker.patch("planetmint.model.fastquery.FastQuery.filter_spent_outputs") - mock_filter_unspent_outputs = mocker.patch("planetmint.model.fastquery.FastQuery.filter_unspent_outputs") + tx_transfer = Transfer.generate(tx.to_inputs([0]), [([alice.public_key], 1)], asset_ids=[tx.id]) + tx_transfer = tx_transfer.sign([alice.private_key]) + b.models.store_bulk_transactions([tx_transfer]) - go = "planetmint.model.fastquery.FastQuery.get_outputs_by_public_key" - with patch(go) as get_outputs: - get_outputs.return_value = [TransactionLink("a", 1), TransactionLink("b", 2)] - out = b.models.get_outputs_filtered("abc") - get_outputs.assert_called_once_with("abc") - mock_filter_spent_outputs.assert_not_called() - mock_filter_unspent_outputs.assert_not_called() - assert out == get_outputs.return_value + outputs = b.models.get_outputs_filtered(alice.public_key) + assert len(outputs) == 3 def test_cant_spend_same_input_twice_in_tx(b, alice): diff --git a/tests/tendermint/test_fastquery.py b/tests/tendermint/test_fastquery.py deleted file mode 100644 index fadd471..0000000 --- a/tests/tendermint/test_fastquery.py +++ /dev/null @@ -1,134 +0,0 @@ -# Copyright © 2020 Interplanetary Database Association e.V., -# Planetmint and IPDB software contributors. -# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) -# Code is Apache-2.0 and docs are CC-BY-4.0 - -import pytest - -from transactions.common.transaction import TransactionLink -from transactions.types.assets.create import Create -from transactions.types.assets.transfer import Transfer - -pytestmark = pytest.mark.bdb - - -@pytest.fixture -def txns(b, user_pk, user_sk, user2_pk, user2_sk, test_models): - txs = [ - Create.generate([user_pk], [([user2_pk], 1)]).sign([user_sk]), - Create.generate([user2_pk], [([user_pk], 1)]).sign([user2_sk]), - Create.generate([user_pk], [([user_pk], 1), ([user2_pk], 1)]).sign([user_sk]), - ] - b.models.store_bulk_transactions(txs) - return txs - - -def test_get_outputs_by_public_key(b, user_pk, user2_pk, txns, test_models): - expected = [TransactionLink(txns[1].id, 0), TransactionLink(txns[2].id, 0)] - actual = test_models.fastquery.get_outputs_by_public_key(user_pk) - - _all_txs = set([tx.txid for tx in expected + actual]) - assert len(_all_txs) == 2 - # assert b.models.fastquery.get_outputs_by_public_key(user_pk) == [ # OLD VERIFICATION - # TransactionLink(txns[1].id, 0), - # TransactionLink(txns[2].id, 0) - # ] - actual_1 = test_models.fastquery.get_outputs_by_public_key(user2_pk) - expected_1 = [ - TransactionLink(txns[0].id, 0), - TransactionLink(txns[2].id, 1), - ] - _all_tx_1 = set([tx.txid for tx in actual_1 + expected_1]) - assert len(_all_tx_1) == 2 - # assert b.models.fastquery.get_outputs_by_public_key(user2_pk) == [ # OLD VERIFICATION - # TransactionLink(txns[0].id, 0), - # TransactionLink(txns[2].id, 1), - # ] - - -def test_filter_spent_outputs(b, user_pk, user_sk, test_models): - out = [([user_pk], 1)] - tx1 = Create.generate([user_pk], out * 2) - tx1.sign([user_sk]) - - inputs = tx1.to_inputs() - - tx2 = Transfer.generate([inputs[0]], out, [tx1.id]) - tx2.sign([user_sk]) - - # tx2 produces a new unspent. inputs[1] remains unspent. - b.models.store_bulk_transactions([tx1, tx2]) - - outputs = test_models.fastquery.get_outputs_by_public_key(user_pk) - unspents = test_models.fastquery.filter_spent_outputs(outputs) - - assert set(unsp for unsp in unspents) == { - inputs[1].fulfills, - tx2.to_inputs()[0].fulfills, - } - - -def test_filter_unspent_outputs(b, user_pk, user_sk, test_models): - out = [([user_pk], 1)] - tx1 = Create.generate([user_pk], out * 2) - tx1.sign([user_sk]) - - inputs = tx1.to_inputs() - - tx2 = Transfer.generate([inputs[0]], out, [tx1.id]) - tx2.sign([user_sk]) - - # tx2 produces a new unspent. input[1] remains unspent. - b.models.store_bulk_transactions([tx1, tx2]) - - outputs = test_models.fastquery.get_outputs_by_public_key(user_pk) - spents = test_models.fastquery.filter_unspent_outputs(outputs) - - assert set(sp for sp in spents) == { - inputs[0].fulfills, - } - - -def test_outputs_query_key_order(b, user_pk, user_sk, user2_pk, user2_sk, test_models, test_validator): - from planetmint import backend - from planetmint.backend.connection import Connection - from planetmint.backend import query - - tx1 = Create.generate([user_pk], [([user_pk], 3), ([user_pk], 2), ([user_pk], 1)]).sign([user_sk]) - b.models.store_bulk_transactions([tx1]) - - inputs = tx1.to_inputs() - tx2 = Transfer.generate([inputs[1]], [([user2_pk], 2)], [tx1.id]).sign([user_sk]) - assert test_validator.validate_transaction(tx2) - - tx2_dict = tx2.to_dict() - fulfills = tx2_dict["inputs"][0]["fulfills"] - tx2_dict["inputs"][0]["fulfills"] = { - "transaction_id": fulfills["transaction_id"], - "output_index": fulfills["output_index"], - } - backend.query.store_transactions(test_models.connection, [tx2_dict]) - - outputs = test_models.get_outputs_filtered(user_pk, spent=False) - assert len(outputs) == 2 - - outputs = test_models.get_outputs_filtered(user2_pk, spent=False) - assert len(outputs) == 1 - - # clean the transaction, metdata and asset collection - connection = Connection() - query.delete_transactions(test_models.connection, txn_ids=[tx1.id, tx2.id]) - - b.models.store_bulk_transactions([tx1]) - tx2_dict = tx2.to_dict() - tx2_dict["inputs"][0]["fulfills"] = { - "output_index": fulfills["output_index"], - "transaction_id": fulfills["transaction_id"], - } - - backend.query.store_transactions(test_models.connection, [tx2_dict]) - outputs = test_models.get_outputs_filtered(user_pk, spent=False) - assert len(outputs) == 2 - - outputs = test_models.get_outputs_filtered(user2_pk, spent=False) - assert len(outputs) == 1 diff --git a/tests/tendermint/test_lib.py b/tests/tendermint/test_lib.py index 7c4fe18..75c60d9 100644 --- a/tests/tendermint/test_lib.py +++ b/tests/tendermint/test_lib.py @@ -22,7 +22,6 @@ from ipld import marshal, multihash from uuid import uuid4 from planetmint.abci.rpc import MODE_COMMIT, MODE_LIST -from tests.utils import delete_unspent_outputs, get_utxoset_merkle_root, store_unspent_outputs, update_utxoset @pytest.mark.bdb @@ -152,17 +151,17 @@ def test_post_transaction_invalid_mode(b, test_abci_rpc): @pytest.mark.bdb def test_update_utxoset(b, signed_create_tx, signed_transfer_tx, db_conn): - update_utxoset(b.models.connection, signed_create_tx) + b.models.update_utxoset(signed_create_tx.to_dict()) utxoset = db_conn.get_space("utxos") assert utxoset.select().rowcount == 1 utxo = utxoset.select().data - assert utxo[0][1] == signed_create_tx.id - assert utxo[0][2] == 0 - update_utxoset(b.models.connection, signed_transfer_tx) + assert utxo[0][5] == signed_create_tx.id + assert utxo[0][4] == 0 + b.models.update_utxoset(signed_transfer_tx.to_dict()) assert utxoset.select().rowcount == 1 utxo = utxoset.select().data - assert utxo[0][1] == signed_transfer_tx.id - assert utxo[0][2] == 0 + assert utxo[0][5] == signed_transfer_tx.id + assert utxo[0][4] == 0 @pytest.mark.bdb @@ -184,107 +183,80 @@ def test_store_bulk_transaction(mocker, b, signed_create_tx, signed_transfer_tx) @pytest.mark.bdb -def test_delete_zero_unspent_outputs(b, utxoset): - unspent_outputs, utxo_collection = utxoset - num_rows_before_operation = utxo_collection.select().rowcount - delete_res = delete_unspent_outputs(b.models.connection) # noqa: F841 - num_rows_after_operation = utxo_collection.select().rowcount - # assert delete_res is None +def test_delete_zero_unspent_outputs(b, alice): + from planetmint.backend.tarantool.sync_io import query + + utxo_space = b.models.connection.get_space("utxos") + + tx = Create.generate([alice.public_key], [([alice.public_key], 8), ([alice.public_key], 1)]).sign( + [alice.private_key] + ) + + b.models.store_bulk_transactions([tx]) + + num_rows_before_operation = utxo_space.select().rowcount + query.delete_unspent_outputs(b.models.connection, []) # noqa: F841 + num_rows_after_operation = utxo_space.select().rowcount assert num_rows_before_operation == num_rows_after_operation @pytest.mark.bdb -def test_delete_one_unspent_outputs(b, dummy_unspent_outputs): +def test_delete_one_unspent_outputs(b, alice): + from planetmint.backend.tarantool.sync_io import query + utxo_space = b.models.connection.get_space("utxos") - for utxo in dummy_unspent_outputs: - res = utxo_space.insert((uuid4().hex, utxo["transaction_id"], utxo["output_index"], utxo)) - assert res - delete_unspent_outputs(b.models.connection, dummy_unspent_outputs[0]) - res1 = utxo_space.select(["a", 1], index="utxo_by_transaction_id_and_output_index").data - res2 = utxo_space.select(["b", 0], index="utxo_by_transaction_id_and_output_index").data - assert len(res1) + len(res2) == 2 - res3 = utxo_space.select(["a", 0], index="utxo_by_transaction_id_and_output_index").data - assert len(res3) == 0 + tx = Create.generate([alice.public_key], [([alice.public_key], 8), ([alice.public_key], 1)]).sign( + [alice.private_key] + ) + + b.models.store_bulk_transactions([tx]) + + query.delete_unspent_outputs(b.models.connection, [{"transaction_id": tx.id, "output_index": 0}]) + res1 = utxo_space.select([tx.id, 1], index="utxo_by_transaction_id_and_output_index").data + res2 = utxo_space.select([tx.id, 0], index="utxo_by_transaction_id_and_output_index").data + assert len(res1) + len(res2) == 1 + assert len(res2) == 0 @pytest.mark.bdb -def test_delete_many_unspent_outputs(b, dummy_unspent_outputs): +def test_delete_many_unspent_outputs(b, alice): + from planetmint.backend.tarantool.sync_io import query + utxo_space = b.models.connection.get_space("utxos") - for utxo in dummy_unspent_outputs: - res = utxo_space.insert((uuid4().hex, utxo["transaction_id"], utxo["output_index"], utxo)) - assert res - delete_unspent_outputs(b.models.connection, *dummy_unspent_outputs[::2]) - res1 = utxo_space.select(["a", 0], index="utxo_by_transaction_id_and_output_index").data - res2 = utxo_space.select(["b", 0], index="utxo_by_transaction_id_and_output_index").data - assert len(res1) + len(res2) == 0 - res3 = utxo_space.select([], index="utxo_by_transaction_id_and_output_index").data - assert len(res3) == 1 + tx = Create.generate( + [alice.public_key], [([alice.public_key], 8), ([alice.public_key], 1), ([alice.public_key], 4)] + ).sign([alice.private_key]) + b.models.store_bulk_transactions([tx]) -@pytest.mark.bdb -def test_store_zero_unspent_output(b): - utxos = b.models.connection.get_space("utxos") - num_rows_before_operation = utxos.select().rowcount - res = store_unspent_outputs(b.models.connection) - num_rows_after_operation = utxos.select().rowcount - assert res is None - assert num_rows_before_operation == num_rows_after_operation - - -@pytest.mark.bdb -def test_store_one_unspent_output(b, unspent_output_1, utxo_collection): - from planetmint.backend.tarantool.sync_io.connection import TarantoolDBConnection - - res = store_unspent_outputs(b.models.connection, unspent_output_1) - if not isinstance(b.models.connection, TarantoolDBConnection): - assert res.acknowledged - assert len(list(res)) == 1 - assert ( - utxo_collection.count_documents( - { - "transaction_id": unspent_output_1["transaction_id"], - "output_index": unspent_output_1["output_index"], - } - ) - == 1 - ) - else: - utx_space = b.models.connection.get_space("utxos") - res = utx_space.select( - [unspent_output_1["transaction_id"], unspent_output_1["output_index"]], - index="utxo_by_transaction_id_and_output_index", - ) - assert len(res.data) == 1 - - -@pytest.mark.bdb -def test_store_many_unspent_outputs(b, unspent_outputs): - store_unspent_outputs(b.models.connection, *unspent_outputs) - utxo_space = b.models.connection.get_space("utxos") - res = utxo_space.select([unspent_outputs[0]["transaction_id"]], index="utxos_by_transaction_id") - assert len(res.data) == 3 + query.delete_unspent_outputs( + b.models.connection, + [{"transaction_id": tx.id, "output_index": 0}, {"transaction_id": tx.id, "output_index": 2}], + ) + res1 = utxo_space.select([tx.id, 1], index="utxo_by_transaction_id_and_output_index").data + res2 = utxo_space.select([tx.id, 0], index="utxo_by_transaction_id_and_output_index").data + assert len(res1) + len(res2) == 1 def test_get_utxoset_merkle_root_when_no_utxo(b): - assert get_utxoset_merkle_root(b.models.connection) == sha3_256(b"").hexdigest() + assert b.models.get_utxoset_merkle_root() == sha3_256(b"").hexdigest() @pytest.mark.bdb -def test_get_utxoset_merkle_root(b, dummy_unspent_outputs): - utxo_space = b.models.connection.get_space("utxos") - for utxo in dummy_unspent_outputs: - res = utxo_space.insert((uuid4().hex, utxo["transaction_id"], utxo["output_index"], utxo)) - assert res +def test_get_utxoset_merkle_root(b, user_sk, user_pk): + tx = Create.generate([user_pk], [([user_pk], 8), ([user_pk], 1), ([user_pk], 4)]).sign([user_sk]) - expected_merkle_root = "86d311c03115bf4d287f8449ca5828505432d69b82762d47077b1c00fe426eac" - merkle_root = get_utxoset_merkle_root(b.models.connection) - assert merkle_root == expected_merkle_root + b.models.store_bulk_transactions([tx]) + + expected_merkle_root = "e5fce6fed606b72744330b28b2f6d68f2eca570c4cf8e3c418b0c3150c75bfe2" + merkle_root = b.models.get_utxoset_merkle_root() + assert merkle_root in expected_merkle_root @pytest.mark.bdb -def test_get_spent_transaction_double_spend(b, alice, bob, carol): +def test_get_spending_transaction_double_spend(b, alice, bob, carol): from transactions.common.exceptions import DoubleSpend assets = [{"data": multihash(marshal({"test": "asset"}))}] @@ -308,15 +280,15 @@ def test_get_spent_transaction_double_spend(b, alice, bob, carol): with pytest.raises(DoubleSpend): b.validate_transaction(same_input_double_spend) - assert b.models.get_spent(tx.id, tx_transfer.inputs[0].fulfills.output, [tx_transfer]) + assert b.models.get_spending_transaction(tx.id, tx_transfer.inputs[0].fulfills.output, [tx_transfer]) with pytest.raises(DoubleSpend): - b.models.get_spent(tx.id, tx_transfer.inputs[0].fulfills.output, [tx_transfer, double_spend]) + b.models.get_spending_transaction(tx.id, tx_transfer.inputs[0].fulfills.output, [tx_transfer, double_spend]) b.models.store_bulk_transactions([tx_transfer]) with pytest.raises(DoubleSpend): - b.models.get_spent(tx.id, tx_transfer.inputs[0].fulfills.output, [double_spend]) + b.models.get_spending_transaction(tx.id, tx_transfer.inputs[0].fulfills.output, [double_spend]) def test_validation_with_transaction_buffer(b): diff --git a/tests/test_core.py b/tests/test_core.py index 8567bf8..fe44dae 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -48,7 +48,7 @@ def test_bigchain_class_default_initialization(config): @pytest.mark.bdb -def test_get_spent_issue_1271(b, alice, bob, carol): +def test_get_spending_transaction_issue_1271(b, alice, bob, carol): tx_1 = Create.generate( [carol.public_key], [([carol.public_key], 8)], @@ -88,7 +88,7 @@ def test_get_spent_issue_1271(b, alice, bob, carol): assert b.validate_transaction(tx_5) b.models.store_bulk_transactions([tx_5]) - assert b.models.get_spent(tx_2.id, 0) == tx_5.to_dict() - assert not b.models.get_spent(tx_5.id, 0) + assert b.models.get_spending_transaction(tx_2.id, 0) == tx_5.to_dict() + assert not b.models.get_spending_transaction(tx_5.id, 0) assert b.models.get_outputs_filtered(alice.public_key) assert b.models.get_outputs_filtered(alice.public_key, spent=False) diff --git a/tests/utils.py b/tests/utils.py index 8fb68f8..5c417c2 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -3,7 +3,6 @@ # SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) # Code is Apache-2.0 and docs are CC-BY-4.0 import multiprocessing -from hashlib import sha3_256 import base58 import base64 @@ -11,7 +10,6 @@ import random from functools import singledispatch -from planetmint import backend from planetmint.backend.localmongodb.connection import LocalMongoDBConnection from planetmint.backend.tarantool.sync_io.connection import TarantoolDBConnection from planetmint.backend.schema import TABLES @@ -20,7 +18,7 @@ from transactions.common.transaction_mode_types import BROADCAST_TX_COMMIT from transactions.types.assets.create import Create from transactions.types.elections.vote import Vote from transactions.types.elections.validator_utils import election_id_to_public_key -from planetmint.abci.utils import merkleroot, key_to_base64 +from planetmint.abci.utils import key_to_base64 from planetmint.abci.rpc import MODE_COMMIT, MODE_LIST @@ -127,78 +125,6 @@ def generate_election(b, cls, public_key, private_key, asset_data, voter_keys): return election, votes -def delete_unspent_outputs(connection, *unspent_outputs): - """Deletes the given ``unspent_outputs`` (utxos). - - Args: - *unspent_outputs (:obj:`tuple` of :obj:`dict`): Variable - length tuple or list of unspent outputs. - """ - if unspent_outputs: - return backend.query.delete_unspent_outputs(connection, *unspent_outputs) - - -def get_utxoset_merkle_root(connection): - """Returns the merkle root of the utxoset. This implies that - the utxoset is first put into a merkle tree. - - For now, the merkle tree and its root will be computed each - time. This obviously is not efficient and a better approach - that limits the repetition of the same computation when - unnecesary should be sought. For instance, future optimizations - could simply re-compute the branches of the tree that were - affected by a change. - - The transaction hash (id) and output index should be sufficient - to uniquely identify a utxo, and consequently only that - information from a utxo record is needed to compute the merkle - root. Hence, each node of the merkle tree should contain the - tuple (txid, output_index). - - .. important:: The leaves of the tree will need to be sorted in - some kind of lexicographical order. - - Returns: - str: Merkle root in hexadecimal form. - """ - utxoset = backend.query.get_unspent_outputs(connection) - # TODO Once ready, use the already pre-computed utxo_hash field. - # See common/transactions.py for details. - hashes = [ - sha3_256("{}{}".format(utxo["transaction_id"], utxo["output_index"]).encode()).digest() for utxo in utxoset - ] - # TODO Notice the sorted call! - return merkleroot(sorted(hashes)) - - -def store_unspent_outputs(connection, *unspent_outputs): - """Store the given ``unspent_outputs`` (utxos). - - Args: - *unspent_outputs (:obj:`tuple` of :obj:`dict`): Variable - length tuple or list of unspent outputs. - """ - if unspent_outputs: - return backend.query.store_unspent_outputs(connection, *unspent_outputs) - - -def update_utxoset(connection, transaction): - """ - Update the UTXO set given ``transaction``. That is, remove - the outputs that the given ``transaction`` spends, and add the - outputs that the given ``transaction`` creates. - - Args: - transaction (:obj:`~planetmint.models.Transaction`): A new - transaction incoming into the system for which the UTXOF - set needs to be updated. - """ - spent_outputs = [spent_output for spent_output in transaction.spent_outputs] - if spent_outputs: - delete_unspent_outputs(connection, *spent_outputs) - store_unspent_outputs(connection, *[utxo._asdict() for utxo in transaction.unspent_outputs]) - - class ProcessGroup(object): def __init__(self, concurrency=None, group=None, target=None, name=None, args=None, kwargs=None, daemon=None): self.concurrency = concurrency or multiprocessing.cpu_count() diff --git a/tests/web/test_transactions.py b/tests/web/test_transactions.py index e4bb04d..d752cd5 100644 --- a/tests/web/test_transactions.py +++ b/tests/web/test_transactions.py @@ -23,10 +23,6 @@ from transactions.common.transaction_mode_types import ( BROADCAST_TX_ASYNC, BROADCAST_TX_SYNC, ) -from transactions.common.transaction import ( - Input, - TransactionLink, -) from transactions.common.utils import _fulfillment_from_details from transactions.common.crypto import generate_key_pair