From 5426732090de92765ad3c17a5f38f40b3015c545 Mon Sep 17 00:00:00 2001 From: Vanshdeep Singh Date: Wed, 25 Jul 2018 14:51:21 +0200 Subject: [PATCH] Problem: Cannot detect transaction type and class dynamically Solution: Create registry for store transaction types and their corresponding classes --- bigchaindb/__init__.py | 8 ++ bigchaindb/common/exceptions.py | 2 +- bigchaindb/common/transaction.py | 79 +++++++++++++------ bigchaindb/models.py | 8 +- bigchaindb/tendermint/lib.py | 8 +- .../upsert_validator/validator_election.py | 22 ++---- bigchaindb/validation_plugins.py | 18 ----- bigchaindb/web/views/transactions.py | 5 +- 8 files changed, 82 insertions(+), 68 deletions(-) delete mode 100644 bigchaindb/validation_plugins.py diff --git a/bigchaindb/__init__.py b/bigchaindb/__init__.py index 011737fa..e49fbee9 100644 --- a/bigchaindb/__init__.py +++ b/bigchaindb/__init__.py @@ -86,3 +86,11 @@ config = { _config = copy.deepcopy(config) from bigchaindb.tendermint import BigchainDB # noqa from bigchaindb.version import __version__ # noqa +from bigchaindb.common.transaction import Transaction # noqa +from bigchaindb import models # noqa +from bigchaindb.upsert_validator import ValidatorElection # noqa + + +Transaction.register_type(Transaction.CREATE, models.Transaction) +Transaction.register_type(Transaction.TRANSFER, models.Transaction) +Transaction.register_type(ValidatorElection.VALIDATOR_ELECTION, ValidatorElection) diff --git a/bigchaindb/common/exceptions.py b/bigchaindb/common/exceptions.py index fadbe784..2e5d1b8c 100644 --- a/bigchaindb/common/exceptions.py +++ b/bigchaindb/common/exceptions.py @@ -104,5 +104,5 @@ class MultipleValidatorOperationError(ValidationError): """Raised when a validator update pending but new request is submited""" -class MultipleInputsError(BigchainDBError): +class MultipleInputsError(ValidationError): """Raised if there were multiple inputs when only one was expected""" diff --git a/bigchaindb/common/transaction.py b/bigchaindb/common/transaction.py index eacb0f08..24a3ae5d 100644 --- a/bigchaindb/common/transaction.py +++ b/bigchaindb/common/transaction.py @@ -585,6 +585,38 @@ class Transaction(object): def _hash(self): self._id = hash_data(self.serialized) + @classmethod + def validate_create(cls, tx_signers, recipients, asset, metadata): + if not isinstance(tx_signers, list): + raise TypeError('`tx_signers` must be a list instance') + if not isinstance(recipients, list): + raise TypeError('`recipients` must be a list instance') + if len(tx_signers) == 0: + raise ValueError('`tx_signers` list cannot be empty') + if len(recipients) == 0: + raise ValueError('`recipients` list cannot be empty') + if not (asset is None or isinstance(asset, dict)): + raise TypeError('`asset` must be a dict or None') + if not (metadata is None or isinstance(metadata, dict)): + raise TypeError('`metadata` must be a dict or None') + + inputs = [] + outputs = [] + + # generate_outputs + for recipient in recipients: + if not isinstance(recipient, tuple) or len(recipient) != 2: + raise ValueError(('Each `recipient` in the list must be a' + ' tuple of `([],' + ' )`')) + pub_keys, amount = recipient + outputs.append(Output.generate(pub_keys, amount)) + + # generate inputs + inputs.append(Input.generate(tx_signers)) + + return (inputs, outputs) + @classmethod def create(cls, tx_signers, recipients, metadata=None, asset=None): """A simple way to generate a `CREATE` transaction. @@ -613,32 +645,8 @@ class Transaction(object): Returns: :class:`~bigchaindb.common.transaction.Transaction` """ - if not isinstance(tx_signers, list): - raise TypeError('`tx_signers` must be a list instance') - if not isinstance(recipients, list): - raise TypeError('`recipients` must be a list instance') - if len(tx_signers) == 0: - raise ValueError('`tx_signers` list cannot be empty') - if len(recipients) == 0: - raise ValueError('`recipients` list cannot be empty') - if not (asset is None or isinstance(asset, dict)): - raise TypeError('`asset` must be a dict or None') - - inputs = [] - outputs = [] - - # generate_outputs - for recipient in recipients: - if not isinstance(recipient, tuple) or len(recipient) != 2: - raise ValueError(('Each `recipient` in the list must be a' - ' tuple of `([],' - ' )`')) - pub_keys, amount = recipient - outputs.append(Output.generate(pub_keys, amount)) - - # generate inputs - inputs.append(Input.generate(tx_signers)) + (inputs, outputs) = cls.validate_create(tx_signers, recipients, asset, metadata) return cls(cls.CREATE, {'data': asset}, inputs, outputs, metadata) @classmethod @@ -1150,7 +1158,7 @@ class Transaction(object): raise InvalidHash(err_msg.format(proposed_tx_id)) @classmethod - def from_dict(cls, tx): + def from_dict(cls, tx, skip_schema_validation=True): """Transforms a Python dictionary to a Transaction object. Args: @@ -1159,6 +1167,11 @@ class Transaction(object): Returns: :class:`~bigchaindb.common.transaction.Transaction` """ + operation = tx.get('operation', Transaction.CREATE) if isinstance(tx, dict) else Transaction.CREATE + cls = Transaction.resolve_class(operation) + if not skip_schema_validation: + cls.validate_schema(tx) + inputs = [Input.from_dict(input_) for input_ in tx['inputs']] outputs = [Output.from_dict(output) for output in tx['outputs']] return cls(tx['operation'], tx['asset'], inputs, outputs, @@ -1213,3 +1226,17 @@ class Transaction(object): else: tx = list(tx_map.values())[0] return cls.from_dict(tx) + + type_registry = {} + + @staticmethod + def register_type(tx_type, tx_class): + Transaction.type_registry[tx_type] = tx_class + + def resolve_class(operation): + """For the given `tx` based on the `operation` key return its implementation class""" + + if operation in list(Transaction.type_registry.keys()): + return Transaction.type_registry.get(operation) + else: + return Transaction.type_registry.get(Transaction.CREATE) diff --git a/bigchaindb/models.py b/bigchaindb/models.py index fcdf8ffa..f3aab491 100644 --- a/bigchaindb/models.py +++ b/bigchaindb/models.py @@ -10,6 +10,7 @@ from bigchaindb.backend.schema import validate_language_key class Transaction(Transaction): + def validate(self, bigchain, current_transactions=[]): """Validate transaction spend @@ -94,12 +95,15 @@ class Transaction(Transaction): @classmethod def from_dict(cls, tx_body): - super().validate_id(tx_body) + return super().from_dict(tx_body, False) + + @classmethod + def validate_schema(cls, tx_body): + cls.validate_id(tx_body) validate_transaction_schema(tx_body) validate_txn_obj('asset', tx_body['asset'], 'data', validate_key) validate_txn_obj('metadata', tx_body, 'metadata', validate_key) validate_language_key(tx_body['asset'], 'data') - return super().from_dict(tx_body) class FastTransaction: diff --git a/bigchaindb/tendermint/lib.py b/bigchaindb/tendermint/lib.py index f46ed916..611f25df 100644 --- a/bigchaindb/tendermint/lib.py +++ b/bigchaindb/tendermint/lib.py @@ -18,7 +18,6 @@ import requests import bigchaindb from bigchaindb import backend, config_utils from bigchaindb.models import Transaction -from bigchaindb.validation_plugins import operation_class from bigchaindb.common.exceptions import (SchemaValidationError, ValidationError, DoubleSpend) @@ -327,7 +326,7 @@ class BigchainDB(object): if len(transactions) + len(current_spent_transactions) > 1: raise DoubleSpend('tx "{}" spends inputs twice'.format(txid)) elif transactions: - transaction = operation_class(transactions[0]).from_db(self, transactions[0]) + transaction = Transaction.from_db(self, transactions[0]) elif current_spent_transactions: transaction = current_spent_transactions[0] @@ -365,8 +364,7 @@ class BigchainDB(object): if block: transactions = backend.query.get_transactions(self.connection, block['transactions']) - cls = operation_class(transactions[0]) - result['transactions'] = [t.to_dict() for t in cls.from_db(self, transactions)] + result['transactions'] = [t.to_dict() for t in Transaction.from_db(self, transactions)] return result @@ -396,7 +394,7 @@ class BigchainDB(object): # throught the code base. if isinstance(transaction, dict): try: - transaction = operation_class(tx).from_dict(tx) + transaction = Transaction.from_dict(tx) except SchemaValidationError as e: logger.warning('Invalid transaction schema: %s', e.__cause__.message) return False diff --git a/bigchaindb/upsert_validator/validator_election.py b/bigchaindb/upsert_validator/validator_election.py index fb182b95..d52cbdd9 100644 --- a/bigchaindb/upsert_validator/validator_election.py +++ b/bigchaindb/upsert_validator/validator_election.py @@ -2,7 +2,7 @@ from bigchaindb.common.exceptions import (InvalidSignature, MultipleInputsError, DuplicateTransaction) from bigchaindb.tendermint.utils import key_from_base64 from bigchaindb.common.crypto import (public_key_from_ed25519_key) -from bigchaindb.common.transaction import Transaction, Input, Output +from bigchaindb.common.transaction import Transaction from bigchaindb.common.schema import (_load_schema, _validate_schema, TX_SCHEMA_VERSION, @@ -22,14 +22,12 @@ class ValidatorElection(Transaction): CREATE = VALIDATOR_ELECTION ALLOWED_OPERATIONS = (VALIDATOR_ELECTION,) - def __init__(self, operation, asset, inputs=None, outputs=None, + def __init__(self, operation, asset, inputs, outputs, metadata=None, version=None, hash_id=None): # operation `CREATE` is being passed as argument as `VALIDATOR_ELECTION` is an extension # of `CREATE` and any validation on `CREATE` in the parent class should apply to it super().__init__(operation, asset, inputs, outputs, metadata, version, hash_id) - self.operation = self.VALIDATOR_ELECTION - @classmethod def current_validators(cls, bigchain): """Return a dictionary of validators with key as `public_key` and @@ -118,22 +116,18 @@ class ValidatorElection(Transaction): @classmethod def generate(cls, initiator, voters, election_data, metadata=None): - election = cls.create(initiator, voters, metadata, asset=election_data) - cls.validate_schema(election.to_dict()) + (inputs, outputs) = cls.validate_create(initiator, voters, election_data, metadata) + election = cls(cls.VALIDATOR_ELECTION, {'data': election_data}, inputs, outputs, metadata) + cls.validate_schema(election.to_dict(), skip_id=True) return election @classmethod - def from_dict(cls, tx): - cls.validate_id(tx) - # NOTE: The schema validation will ensure that the asset has been properly defined - cls.validate_schema(tx) - return super().from_dict(tx) - - @classmethod - def validate_schema(cls, tx): + def validate_schema(cls, tx, skip_id=False): """Validate the validator election transaction. Since `VALIDATOR_ELECTION` extends `CREATE` transaction, all the validations for `CREATE` transaction should be inherited """ + if not skip_id: + cls.validate_id(tx) _validate_schema(TX_SCHEMA_COMMON, tx) _validate_schema(TX_SCHEMA_CREATE, tx) _validate_schema(TX_SCHEMA_VALIDATOR_ELECTION, tx) diff --git a/bigchaindb/validation_plugins.py b/bigchaindb/validation_plugins.py deleted file mode 100644 index 19d85b15..00000000 --- a/bigchaindb/validation_plugins.py +++ /dev/null @@ -1,18 +0,0 @@ -from bigchaindb.models import Transaction -from bigchaindb.upsert_validator import ValidatorElection - - -OPERATION_TO_CLASS = { - Transaction.CREATE: Transaction, - Transaction.TRANSFER: Transaction, - ValidatorElection.VALIDATOR_ELECTION: ValidatorElection -} - - -def operation_class(tx): - """For the given `tx` based on the `operation` key return its implementation class""" - - if isinstance(tx, dict): - return OPERATION_TO_CLASS.get(tx.get('operation', Transaction.CREATE), Transaction) - else: - return Transaction diff --git a/bigchaindb/web/views/transactions.py b/bigchaindb/web/views/transactions.py index 404ebdc7..95a0378e 100644 --- a/bigchaindb/web/views/transactions.py +++ b/bigchaindb/web/views/transactions.py @@ -10,7 +10,8 @@ from flask_restful import Resource, reqparse from bigchaindb.common.exceptions import SchemaValidationError, ValidationError from bigchaindb.web.views.base import make_error from bigchaindb.web.views import parameters -from bigchaindb.validation_plugins import operation_class +from bigchaindb.common.transaction import Transaction + logger = logging.getLogger(__name__) @@ -68,7 +69,7 @@ class TransactionListApi(Resource): tx = request.get_json(force=True) try: - tx_obj = operation_class(tx).from_dict(tx) + tx_obj = Transaction.from_dict(tx) except SchemaValidationError as e: return make_error( 400,