Problem: Cannot detect transaction type and class dynamically

Solution: Create registry for store transaction types and their corresponding classes
This commit is contained in:
Vanshdeep Singh 2018-07-25 14:51:21 +02:00
parent 6a00c0823f
commit 5426732090
8 changed files with 82 additions and 68 deletions

View File

@ -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)

View File

@ -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"""

View File

@ -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 `([<list of public keys>],'
' <amount>)`'))
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 `([<list of public keys>],'
' <amount>)`'))
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)

View File

@ -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:

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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,