added initial interfaces for backend, refactored Asset and MetaData logic

Signed-off-by: Lorenz Herzberger <lorenzherzberger@gmail.com>
This commit is contained in:
Lorenz Herzberger 2022-11-14 14:43:14 +01:00
parent 713bd5267c
commit 2694974a37
No known key found for this signature in database
GPG Key ID: FA5EE906EB55316A
7 changed files with 132 additions and 82 deletions

View File

@ -0,0 +1,70 @@
# 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 dataclasses import dataclass
# Asset should represent a single asset (e.g.: tarantool tuple (data, tx_id, asset_id))
# If multiple assets are stored at once this should remain the same.
# For Create ({'data': 'values'}, c_tx_id, c_tx_id), For Transfer ({'id': c_tx_id}, tx_id, c_tx_id)
@dataclass
class Asset:
id: str = None
tx_id: str = None
data: dict = None
@dataclass
class MetaData:
id: str = None
metadata: str = None
@dataclass
class Input:
id: str = None
@dataclass
class Output:
id: str = None
@dataclass
class Block:
id: str = None
app_hash: str = None
height: int = None
@dataclass
class Script:
id: str = None
script = None
@dataclass
class UTXO:
id: str = None
output_index: int = None
utxo: dict = None
@dataclass
class Transaction:
id: str = None
assets: list[Asset] = None
metadata: MetaData = None
version: str = None # TODO: make enum
operation: str = None # TODO: make enum
inputs: list[Input] = None
outputs: list[Output] = None
script: str = None
@dataclass
class Validator:
id: str = None
height: int = None
validators = None
@dataclass
class ABCIChain:
height: str = None
is_synced: bool = None
chain_id: str = None

View File

@ -7,11 +7,10 @@
from functools import singledispatch from functools import singledispatch
from planetmint.backend.exceptions import OperationError from planetmint.backend.exceptions import OperationError
from planetmint.backend.interfaces import Asset, MetaData
# FIXME ADD HERE HINT FOR RETURNING TYPE
@singledispatch @singledispatch
def store_asset(asset: dict, connection): def store_asset(connection, asset: dict) -> Asset:
"""Write an asset to the asset table. """Write an asset to the asset table.
Args: Args:
@ -25,7 +24,7 @@ def store_asset(asset: dict, connection):
@singledispatch @singledispatch
def store_assets(assets: list, connection): def store_assets(connection, assets: list) -> list[Asset]:
"""Write a list of assets to the assets table. """Write a list of assets to the assets table.
backend backend
Args: Args:
@ -39,7 +38,7 @@ def store_assets(assets: list, connection):
@singledispatch @singledispatch
def store_metadatas(connection, metadata): def store_metadatas(connection, metadata) -> MetaData:
"""Write a list of metadata to metadata table. """Write a list of metadata to metadata table.
Args: Args:
@ -88,7 +87,7 @@ def get_transactions(connection, transaction_ids):
@singledispatch @singledispatch
def get_asset(connection, asset_id): def get_asset(connection, asset_id) -> Asset:
"""Get an asset from the assets table. """Get an asset from the assets table.
Args: Args:
@ -191,7 +190,7 @@ def get_metadata(connection, transaction_ids):
@singledispatch @singledispatch
def get_assets(connection, asset_ids) -> list: def get_assets(connection, asset_ids) -> list[Asset]:
"""Get a list of assets from the assets table. """Get a list of assets from the assets table.
Args: Args:
asset_ids (list): a list of ids for the assets to be retrieved from asset_ids (list): a list of ids for the assets to be retrieved from

View File

@ -12,10 +12,10 @@ from operator import itemgetter
from tarantool.error import DatabaseError from tarantool.error import DatabaseError
from planetmint.backend import query from planetmint.backend import query
from planetmint.backend.utils import module_dispatch_registrar from planetmint.backend.utils import module_dispatch_registrar
from planetmint.backend.interfaces import Asset, MetaData
from planetmint.backend.tarantool.connection import TarantoolDBConnection from planetmint.backend.tarantool.connection import TarantoolDBConnection
from planetmint.backend.tarantool.transaction.tools import TransactionCompose, TransactionDecompose from planetmint.backend.tarantool.transaction.tools import TransactionCompose, TransactionDecompose
register_query = module_dispatch_registrar(query) register_query = module_dispatch_registrar(query)
@ -91,11 +91,11 @@ def get_transactions(connection, transactions_ids: list):
@register_query(TarantoolDBConnection) @register_query(TarantoolDBConnection)
def store_metadatas(connection, metadata: list): def store_metadatas(connection, metadata: list[MetaData]):
for meta in metadata: for meta in metadata:
connection.run( connection.run(
connection.space("meta_data").insert( connection.space("meta_data").insert(
(meta["id"], json.dumps(meta["data"] if not "metadata" in meta else meta["metadata"])) (meta.id, json.dumps(meta.metadata))
) # noqa: E713 ) # noqa: E713
) )
@ -115,17 +115,10 @@ def get_metadata(connection, transaction_ids: list):
@register_query(TarantoolDBConnection) @register_query(TarantoolDBConnection)
def store_asset(connection, asset): def store_asset(connection, asset: Asset):
def convert(obj): asset = (json.dumps(asset.data), asset.tx_id, asset.id)
if isinstance(obj, tuple):
obj = list(obj)
obj[0] = json.dumps(obj[0])
return tuple(obj)
else:
return (json.dumps(obj), obj["id"], obj["id"])
try: try:
return connection.run(connection.space("assets").insert(convert(asset)), only_data=False) return connection.run(connection.space("assets").insert(asset), only_data=False)
except DatabaseError: except DatabaseError:
pass pass
@ -137,21 +130,20 @@ def store_assets(connection, assets: list):
@register_query(TarantoolDBConnection) @register_query(TarantoolDBConnection)
def get_asset(connection, asset_id: str): def get_asset(connection, asset_id: str) -> Asset:
_data = connection.run(connection.space("assets").select(asset_id, index="txid_search")) _data = connection.run(connection.space("assets").select(asset_id, index="txid_search"))
return Asset(_data[0][2], _data[0][1], json.loads(_data[0][0]))
return json.loads(_data[0][0]) if len(_data) > 0 else []
@register_query(TarantoolDBConnection) @register_query(TarantoolDBConnection)
def get_assets(connection, assets_ids: list) -> list: def get_assets(connection, assets_ids: list) -> list[Asset]:
_returned_data = [] _returned_data = []
for _id in list(set(assets_ids)): for _id in list(set(assets_ids)):
res = connection.run(connection.space("assets").select(_id, index="txid_search")) res = connection.run(connection.space("assets").select(_id, index="txid_search"))
_returned_data.append(res[0]) _returned_data.append(res[0])
sorted_assets = sorted(_returned_data, key=lambda k: k[1], reverse=False) sorted_assets = sorted(_returned_data, key=lambda k: k[1], reverse=False)
return [(json.loads(asset[0]), asset[1]) for asset in sorted_assets] return [Asset(asset[2], asset[1], json.loads(asset[0])) for asset in sorted_assets]
@register_query(TarantoolDBConnection) @register_query(TarantoolDBConnection)
@ -238,7 +230,7 @@ def text_search(conn, search, table="assets", limit=0):
if len(res[0]): # NEEDS BEAUTIFICATION if len(res[0]): # NEEDS BEAUTIFICATION
if table == "assets": if table == "assets":
for result in res[0]: for result in res[0]:
to_return.append({"data": json.loads(result[0])[0]["data"], "id": result[1]}) to_return.append({"data": json.loads(result[0])["data"], "id": result[1]})
else: else:
for result in res[0]: for result in res[0]:
to_return.append({"metadata": json.loads(result[1]), "id": result[0]}) to_return.append({"metadata": json.loads(result[1]), "id": result[0]})

View File

@ -54,6 +54,7 @@ from planetmint.tendermint_utils import (
) )
from planetmint import exceptions as core_exceptions from planetmint import exceptions as core_exceptions
from planetmint.validation import BaseValidationRules from planetmint.validation import BaseValidationRules
from planetmint.backend.interfaces import Asset, MetaData
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -149,20 +150,15 @@ class Planetmint(object):
tx_assets = transaction.pop("assets") tx_assets = transaction.pop("assets")
metadata = transaction.pop("metadata") metadata = transaction.pop("metadata")
tx_assets = backend.convert.prepare_asset( if tx_assets is not None:
self.connection, for asset in tx_assets:
transaction_type=transaction["operation"], id = transaction["id"] if "id" not in asset else asset["id"]
transaction_id=transaction["id"], tx_asset = Asset(id, transaction["id"], asset)
filter_operation=[t.CREATE, t.VALIDATOR_ELECTION, t.CHAIN_MIGRATION_ELECTION], assets.append(tx_asset)
assets=tx_assets,
)
metadata = backend.convert.prepare_metadata( metadata = MetaData(transaction["id"], metadata)
self.connection, transaction_id=transaction["id"], metadata=metadata
)
txn_metadatas.append(metadata) txn_metadatas.append(metadata)
assets.append(tx_assets)
txns.append(transaction) txns.append(transaction)
backend.query.store_metadatas(self.connection, txn_metadatas) backend.query.store_metadatas(self.connection, txn_metadatas)
@ -258,10 +254,7 @@ class Planetmint(object):
if transaction: if transaction:
assets = backend.query.get_assets(self.connection, [transaction_id]) assets = backend.query.get_assets(self.connection, [transaction_id])
metadata = backend.query.get_metadata(self.connection, [transaction_id]) metadata = backend.query.get_metadata(self.connection, [transaction_id])
# NOTE: assets must not be replaced for transfer transactions transaction["assets"] = [asset.data for asset in assets]
# NOTE: assets should be appended for all txs that define new assets otherwise the ids are already stored in tx
if transaction["operation"] != "TRANSFER" and transaction["operation"] != "VOTE" and assets:
transaction["assets"] = assets[0][0]
if "metadata" not in transaction: if "metadata" not in transaction:
metadata = metadata[0] if metadata else None metadata = metadata[0] if metadata else None
@ -323,6 +316,8 @@ class Planetmint(object):
raise DoubleSpend('tx "{}" spends inputs twice'.format(txid)) raise DoubleSpend('tx "{}" spends inputs twice'.format(txid))
elif transactions: elif transactions:
transaction = backend.query.get_transaction(self.connection, transactions[0]["id"]) transaction = backend.query.get_transaction(self.connection, transactions[0]["id"])
assets = backend.query.get_assets(self.connection, [transaction["id"]])
transaction["assets"] = [asset.data for asset in assets]
transaction = Transaction.from_dict(transaction, False) transaction = Transaction.from_dict(transaction, False)
elif current_spent_transactions: elif current_spent_transactions:
transaction = current_spent_transactions[0] transaction = current_spent_transactions[0]
@ -476,7 +471,7 @@ class Planetmint(object):
""" """
return backend.query.text_search(self.connection, search, limit=limit, table=table) return backend.query.text_search(self.connection, search, limit=limit, table=table)
def get_assets(self, asset_ids): def get_assets(self, asset_ids) -> list[Asset]:
"""Return a list of assets that match the asset_ids """Return a list of assets that match the asset_ids
Args: Args:
@ -597,12 +592,12 @@ class Planetmint(object):
tx_map[tx["id"]] = tx tx_map[tx["id"]] = tx
tx_ids.append(tx["id"]) tx_ids.append(tx["id"])
assets = list(self.get_assets(tx_ids)) # TODO: this will break if more than one asset is used
assets = self.get_assets(tx_ids)
for asset in assets: for asset in assets:
if asset is not None: if asset is not None:
# This is tarantool specific behaviour needs to be addressed tx = tx_map[asset.id]
tx = tx_map[asset[1]] tx["assets"] = [asset.data]
tx["asset"] = asset[0]
tx_ids = list(tx_map.keys()) tx_ids = list(tx_map.keys())
metadata_list = list(self.get_metadata(tx_ids)) metadata_list = list(self.get_metadata(tx_ids))

View File

@ -137,7 +137,7 @@ install_requires = [
"pyasn1>=0.4.8", "pyasn1>=0.4.8",
"cryptography==3.4.7", "cryptography==3.4.7",
"python-decouple", "python-decouple",
"planetmint-transactions==0.2.0", "planetmint-transactions==0.2.1",
] ]
setup( setup(

View File

@ -10,6 +10,7 @@ import json
from transactions.common.transaction import Transaction from transactions.common.transaction import Transaction
from transactions.types.assets.create import Create from transactions.types.assets.create import Create
from transactions.types.assets.transfer import Transfer from transactions.types.assets.transfer import Transfer
from planetmint.backend.interfaces import Asset, MetaData
pytestmark = pytest.mark.bdb pytestmark = pytest.mark.bdb
@ -44,11 +45,11 @@ def test_write_assets(db_conn):
from planetmint.backend.tarantool import query from planetmint.backend.tarantool import query
assets = [ assets = [
{"id": "1", "data": "1"}, Asset("1", "1", "1"),
{"id": "2", "data": "2"}, Asset("2", "2", "2"),
{"id": "3", "data": "3"}, Asset("3", "3", "3"),
# Duplicated id. Should not be written to the database # Duplicated id. Should not be written to the database
{"id": "1", "data": "1"}, Asset("1", "1", "1")
] ]
# write the assets # write the assets
@ -56,25 +57,25 @@ def test_write_assets(db_conn):
query.store_asset(connection=db_conn, asset=asset) query.store_asset(connection=db_conn, asset=asset)
# check that 3 assets were written to the database # check that 3 assets were written to the database
documents = query.get_assets(assets_ids=[asset["id"] for asset in assets], connection=db_conn) documents = query.get_assets(assets_ids=[asset.id for asset in assets], connection=db_conn)
assert len(documents) == 3 assert len(documents) == 3
assert list(documents)[0][0] == assets[:-1][0] assert list(documents)[0] == assets[:-1][0]
def test_get_assets(db_conn): def test_get_assets(db_conn):
from planetmint.backend.tarantool import query from planetmint.backend.tarantool import query
assets = [ assets = [
("1", "1", "1"), Asset("1", "1"),
("2", "2", "2"), Asset("2", "2"),
("3", "3", "3"), Asset("3", "3"),
] ]
query.store_assets(assets=assets, connection=db_conn) query.store_assets(assets=assets, connection=db_conn)
for asset in assets: for asset in assets:
assert query.get_asset(asset_id=asset[2], connection=db_conn) assert query.get_asset(asset_id=asset.id, connection=db_conn)
@pytest.mark.parametrize("table", ["assets", "metadata"]) @pytest.mark.parametrize("table", ["assets", "metadata"])
@ -164,17 +165,17 @@ def test_text_search(table):
def test_write_metadata(db_conn): def test_write_metadata(db_conn):
from planetmint.backend.tarantool import query from planetmint.backend.tarantool import query
metadata = [{"id": "1", "data": "1"}, {"id": "2", "data": "2"}, {"id": "3", "data": "3"}] metadata = [MetaData("1", "1"), MetaData("2", "2"), MetaData("3", "3")]
# write the assets # write the assets
query.store_metadatas(connection=db_conn, metadata=metadata) query.store_metadatas(connection=db_conn, metadata=metadata)
# check that 3 assets were written to the database # check that 3 assets were written to the database
metadatas = [] metadatas = []
for meta in metadata: for meta in metadata:
_data = db_conn.run(db_conn.space("meta_data").select(meta["id"]))[0] _data = db_conn.run(db_conn.space("meta_data").select(meta.id))[0]
metadatas.append({"id": _data[0], "data": json.loads(_data[1])}) metadatas.append(MetaData(_data[0], json.loads(_data[1])))
metadatas = sorted(metadatas, key=lambda k: k["id"]) metadatas = sorted(metadatas, key=lambda k: k.id)
assert len(metadatas) == 3 assert len(metadatas) == 3
assert list(metadatas) == metadata assert list(metadatas) == metadata
@ -184,15 +185,15 @@ def test_get_metadata(db_conn):
from planetmint.backend.tarantool import query from planetmint.backend.tarantool import query
metadata = [ metadata = [
{"id": "dd86682db39e4b424df0eec1413cfad65488fd48712097c5d865ca8e8e059b64", "metadata": None}, MetaData("dd86682db39e4b424df0eec1413cfad65488fd48712097c5d865ca8e8e059b64", None),
{"id": "55a2303e3bcd653e4b5bd7118d39c0e2d48ee2f18e22fbcf64e906439bdeb45d", "metadata": {"key": "value"}}, MetaData("55a2303e3bcd653e4b5bd7118d39c0e2d48ee2f18e22fbcf64e906439bdeb45d", {"key": "value"})
] ]
# conn.db.metadata.insert_many(deepcopy(metadata), ordered=False) # conn.db.metadata.insert_many(deepcopy(metadata), ordered=False)
query.store_metadatas(connection=db_conn, metadata=metadata) query.store_metadatas(connection=db_conn, metadata=metadata)
for meta in metadata: for meta in metadata:
_m = query.get_metadata(connection=db_conn, transaction_ids=[meta["id"]]) _m = query.get_metadata(connection=db_conn, transaction_ids=[meta.id])
assert _m assert _m

View File

@ -20,6 +20,7 @@ from transactions.common.transaction_mode_types import (
BROADCAST_TX_SYNC, BROADCAST_TX_SYNC,
) )
from planetmint.lib import Block from planetmint.lib import Block
from planetmint.backend.interfaces import Asset, MetaData
from ipld import marshal, multihash from ipld import marshal, multihash
@ -195,12 +196,12 @@ def test_store_transaction(mocker, b, signed_create_tx, signed_transfer_tx, db_c
) )
else: else:
mocked_store_asset.assert_called_once_with( mocked_store_asset.assert_called_once_with(
b.connection, [(signed_create_tx.assets, signed_create_tx.id, signed_create_tx.id)] b.connection, [Asset(signed_create_tx.id, signed_create_tx.id, signed_create_tx.assets[0])]
) )
mocked_store_metadata.assert_called_once_with( mocked_store_metadata.assert_called_once_with(
b.connection, b.connection,
[{"id": signed_create_tx.id, "metadata": signed_create_tx.metadata}], [MetaData(signed_create_tx.id, signed_create_tx.metadata)]
) )
mocked_store_transaction.assert_called_once_with( mocked_store_transaction.assert_called_once_with(
b.connection, b.connection,
@ -245,7 +246,7 @@ def test_store_bulk_transaction(mocker, b, signed_create_tx, signed_transfer_tx,
if isinstance(b.connection, TarantoolDBConnection): if isinstance(b.connection, TarantoolDBConnection):
mocked_store_assets.assert_called_once_with( mocked_store_assets.assert_called_once_with(
b.connection, # signed_create_tx.asset['data'] this was before b.connection, # signed_create_tx.asset['data'] this was before
[(signed_create_tx.assets, signed_create_tx.id, signed_create_tx.id)], [Asset(signed_create_tx.id, signed_create_tx.id, signed_create_tx.assets[0])]
) )
else: else:
mocked_store_assets.assert_called_once_with( mocked_store_assets.assert_called_once_with(
@ -254,7 +255,7 @@ def test_store_bulk_transaction(mocker, b, signed_create_tx, signed_transfer_tx,
) )
mocked_store_metadata.assert_called_once_with( mocked_store_metadata.assert_called_once_with(
b.connection, b.connection,
[{"id": signed_create_tx.id, "metadata": signed_create_tx.metadata}], [MetaData(signed_create_tx.id, signed_create_tx.metadata)]
) )
mocked_store_transactions.assert_called_once_with( mocked_store_transactions.assert_called_once_with(
b.connection, b.connection,
@ -513,28 +514,20 @@ def test_migrate_abci_chain_generates_new_chains(b, chain, block_height, expecte
@pytest.mark.bdb @pytest.mark.bdb
def test_get_spent_key_order(b, user_pk, user_sk, user2_pk, user2_sk): def test_get_spent_key_order(b, user_pk, user_sk, user2_pk, user2_sk):
from planetmint import backend
from transactions.common.crypto import generate_key_pair from transactions.common.crypto import generate_key_pair
from transactions.common.exceptions import DoubleSpend from transactions.common.exceptions import DoubleSpend
alice = generate_key_pair() alice = generate_key_pair()
bob = generate_key_pair() bob = generate_key_pair()
tx1 = Create.generate([user_pk], [([alice.public_key], 3), ([user_pk], 2)], assets=None).sign([user_sk]) tx1 = Create.generate([user_pk], [([alice.public_key], 3), ([user_pk], 2)]).sign([user_sk])
b.store_bulk_transactions([tx1]) b.store_bulk_transactions([tx1])
inputs = tx1.to_inputs() inputs = tx1.to_inputs()
tx2 = Transfer.generate([inputs[1]], [([user2_pk], 2)], [tx1.id]).sign([user_sk]) tx2 = Transfer.generate([inputs[1]], [([user2_pk], 2)], [tx1.id]).sign([user_sk])
assert b.validate_transaction(tx2) assert b.validate_transaction(tx2)
tx2_dict = tx2.to_dict() b.store_bulk_transactions([tx2])
fulfills = tx2_dict["inputs"][0]["fulfills"]
tx2_dict["inputs"][0]["fulfills"] = {
"output_index": fulfills["output_index"],
"transaction_id": fulfills["transaction_id"],
}
backend.query.store_transactions(b.connection, [tx2_dict])
tx3 = Transfer.generate([inputs[1]], [([bob.public_key], 2)], [tx1.id]).sign([user_sk]) tx3 = Transfer.generate([inputs[1]], [([bob.public_key], 2)], [tx1.id]).sign([user_sk])