From a5243e43f68661d99ea68c6b788fa1838eff7895 Mon Sep 17 00:00:00 2001 From: Matt Smith Date: Tue, 8 Mar 2016 18:33:31 -0800 Subject: [PATCH] This large (sorry) commit 1. switches from a composable plugin model to a single-plugin model 2. switches class methods to static methods in the BaseConsensusRules class 3. adds create_transaction, sign_transaction, and verify_transaction to the plugin API TODO: If we adopt this model, all references in e.g. client.py to util methods like `sign_tx` need to be routed through the plugin methods, and possibly need to be added to the plugin interface. --- bigchaindb/config_utils.py | 1 + bigchaindb/consensus.py | 189 +++++++++++++++++++++++++------------ bigchaindb/core.py | 60 ++++++------ setup.py | 4 +- 4 files changed, 159 insertions(+), 95 deletions(-) diff --git a/bigchaindb/config_utils.py b/bigchaindb/config_utils.py index 7f312d0c..26afa3c8 100644 --- a/bigchaindb/config_utils.py +++ b/bigchaindb/config_utils.py @@ -20,6 +20,7 @@ import collections from pkg_resources import iter_entry_points, ResolutionError import bigchaindb +from bigchaindb.consensus import AbstractConsensusRules logger = logging.getLogger(__name__) CONFIG_DEFAULT_PATH = os.environ.setdefault( diff --git a/bigchaindb/consensus.py b/bigchaindb/consensus.py index 1c0b1aec..818249b8 100644 --- a/bigchaindb/consensus.py +++ b/bigchaindb/consensus.py @@ -19,7 +19,7 @@ class AbstractConsensusRules(metaclass=ABCMeta): """Validate a transaction. Args: - bigchain (Bigchain): an instantiated bigchaindb.Bigchain object. + bigchain (Bigchain): an instantiated ``bigchaindb.Bigchain`` object. transaction (dict): transaction to validate. Returns: @@ -37,7 +37,7 @@ class AbstractConsensusRules(metaclass=ABCMeta): """Validate a block. Args: - bigchain (Bigchain): an instantiated bigchaindb.Bigchain object. + bigchain (Bigchain): an instantiated ``bigchaindb.Bigchain`` object. block (dict): block to validate. Returns: @@ -48,109 +48,174 @@ class AbstractConsensusRules(metaclass=ABCMeta): Descriptive exceptions indicating the reason the block failed. See the `exceptions` module for bigchain-native error classes. """ - return block + raise NotImplementedError - class ConsensusRules(AbstractConsensusRules): + @abstractmethod + def create_transaction(*args, **kwargs): + """Create a new transaction. + + Args: + The signature of this method is left to plugin authors to decide. + + Returns: + dict: newly constructed transaction. + """ + raise NotImplementedError + + @abstractmethod + def sign_transaction(transaction, *args, **kwargs): + """Sign a transaction. + + Args: + transaction (dict): transaction to sign. + any other arguments are left to plugin authors to decide. + + Returns: + dict: transaction with any signatures applied. + """ + raise NotImplementedError + + @abstractmethod + def verify_signature(signed_transaction): + """Verify the signature of a transaction. + + Args: + signed_transaction (dict): signed transaction to verify + + Returns: + bool: True if the transaction's required signature data is present + and correct, False otherwise. + """ + raise NotImplementedError + + +class BaseConsensusRules(AbstractConsensusRules): """Base consensus rules for Bigchain. - This class can be copied to write your own consensus rules! - - Note: Consensus plugins will be executed in the order that they're listed in - the bigchain config file. + This class can be copied or overridden to write your own consensus rules! """ - @classmethod - def validate_transaction(cls, bigchain, transaction): + @staticmethod + def validate_transaction(bigchain, transaction): """Validate a transaction. Args: - bigchain (Bigchain): an instantiated bigchaindb.Bigchain object. - transaction (dict): transaction to validate. + bigchain (Bigchain): an instantiated bigchaindb.Bigchain object. + transaction (dict): transaction to validate. Returns: - The transaction if the transaction is valid else it raises an - exception describing the reason why the transaction is invalid. + The transaction if the transaction is valid else it raises an + exception describing the reason why the transaction is invalid. Raises: - OperationError: if the transaction operation is not supported - TransactionDoesNotExist: if the input of the transaction is not found - TransactionOwnerError: if the new transaction is using an input it doesn't own - DoubleSpend: if the transaction is a double spend - InvalidHash: if the hash of the transaction is wrong - InvalidSignature: if the signature of the transaction is wrong + OperationError: if the transaction operation is not supported + TransactionDoesNotExist: if the input of the transaction is not found + TransactionOwnerError: if the new transaction is using an input it doesn't own + DoubleSpend: if the transaction is a double spend + InvalidHash: if the hash of the transaction is wrong + InvalidSignature: if the signature of the transaction is wrong """ # If the operation is CREATE the transaction should have no inputs and # should be signed by a federation node if transaction['transaction']['operation'] == 'CREATE': - if transaction['transaction']['input']: - raise ValueError('A CREATE operation has no inputs') - if transaction['transaction']['current_owner'] not in ( - bigchain.federation_nodes + [bigchain.me]): - raise exceptions.OperationError( - 'Only federation nodes can use the operation `CREATE`') + if transaction['transaction']['input']: + raise ValueError('A CREATE operation has no inputs') + if transaction['transaction']['current_owner'] not in ( + bigchain.federation_nodes + [bigchain.me]): + raise exceptions.OperationError( + 'Only federation nodes can use the operation `CREATE`') else: - # check if the input exists, is owned by the current_owner - if not transaction['transaction']['input']: - raise ValueError( - 'Only `CREATE` transactions can have null inputs') + # check if the input exists, is owned by the current_owner + if not transaction['transaction']['input']: + raise ValueError( + 'Only `CREATE` transactions can have null inputs') - tx_input = bigchain.get_transaction( - transaction['transaction']['input']) + tx_input = bigchain.get_transaction( + transaction['transaction']['input']) - if not tx_input: - raise exceptions.TransactionDoesNotExist( - 'input `{}` does not exist in the bigchain'.format( - transaction['transaction']['input'])) + if not tx_input: + raise exceptions.TransactionDoesNotExist( + 'input `{}` does not exist in the bigchain'.format( + transaction['transaction']['input'])) - if (tx_input['transaction']['new_owner'] != - transaction['transaction']['current_owner']): - raise exceptions.TransactionOwnerError( - 'current_owner `{}` does not own the input `{}`'.format( - transaction['transaction']['current_owner'], - transaction['transaction']['input'])) + if (tx_input['transaction']['new_owner'] != + transaction['transaction']['current_owner']): + raise exceptions.TransactionOwnerError( + 'current_owner `{}` does not own the input `{}`'.format( + transaction['transaction']['current_owner'], + transaction['transaction']['input'])) - # check if the input was already spent by a transaction other than - # this one. - spent = bigchain.get_spent(tx_input['id']) - if spent and spent['id'] != transaction['id']: - raise exceptions.DoubleSpend( - 'input `{}` was already spent'.format( - transaction['transaction']['input'])) + # check if the input was already spent by a transaction other than + # this one. + spent = bigchain.get_spent(tx_input['id']) + if spent and spent['id'] != transaction['id']: + raise exceptions.DoubleSpend( + 'input `{}` was already spent'.format( + transaction['transaction']['input'])) # Check hash of the transaction calculated_hash = hash_data(util.serialize( - transaction['transaction'])) + transaction['transaction'])) if calculated_hash != transaction['id']: - raise exceptions.InvalidHash() + raise exceptions.InvalidHash() # Check signature - if not bigchain.verify_signature(transaction): - raise exceptions.InvalidSignature() + if not util.verify_signature(transaction): + raise exceptions.InvalidSignature() return transaction - # TODO: check that the votings structure is correctly constructed - @classmethod - def validate_block(cls, bigchain, block): + # TODO: Unsure if a bigchain parameter is really necessary here? + @staticmethod + def validate_block(bigchain, block): """Validate a block. Args: - bigchain (Bigchain): an instantiated bigchaindb.Bigchain object. - block (dict): block to validate. + bigchain (Bigchain): an instantiated bigchaindb.Bigchain object. + block (dict): block to validate. Returns: - The block if the block is valid else it raises and exception - describing the reason why the block is invalid. + The block if the block is valid else it raises and exception + describing the reason why the block is invalid. Raises: - InvalidHash: if the hash of the block is wrong. + InvalidHash: if the hash of the block is wrong. """ # Check if current hash is correct calculated_hash = hash_data(util.serialize(block['block'])) if calculated_hash != block['id']: - raise exceptions.InvalidHash() + raise exceptions.InvalidHash() return block + + @staticmethod + def create_transaction(current_owner, new_owner, tx_input, operation, + payload=None): + """Create a new transaction + + Refer to the documentation of ``bigchaindb.util.create_tx`` + """ + + return util.create_tx(current_owner, new_owner, tx_input, operation, + payload) + + @staticmethod + def sign_transaction(transaction, private_key): + """Sign a transaction + + Refer to the documentation of ``bigchaindb.util.sign_tx`` + """ + + return util.sign_tx(transaction, private_key) + + @staticmethod + def verify_signature(signed_transaction): + """Verify the signature of a transaction. + + Refer to the documentation of ``bigchaindb.util.verify_signature`` + """ + + return util.verify_signature(signed_transaction) diff --git a/bigchaindb/core.py b/bigchaindb/core.py index ffdffa9b..f76dd2d6 100644 --- a/bigchaindb/core.py +++ b/bigchaindb/core.py @@ -27,7 +27,7 @@ class Bigchain(object): def __init__(self, host=None, port=None, dbname=None, public_key=None, private_key=None, keyring=[], - consensus_plugins=['base']): + consensus_plugin=None): """Initialize the Bigchain instance There are three ways in which the Bigchain instance can get its parameters. @@ -53,7 +53,7 @@ class Bigchain(object): self.me = public_key or bigchaindb.config['keypair']['public'] self.me_private = private_key or bigchaindb.config['keypair']['private'] self.federation_nodes = keyring or bigchaindb.config['keyring'] - self.consensus_plugins = config_utils.get_plugins(consensus_plugins) + self.consensus = config_utils.load_consensus_plugin(consensus_plugin) if not self.me or not self.me_private: raise exceptions.KeypairNotFoundException() @@ -70,38 +70,40 @@ class Bigchain(object): return r.connect(host=self.host, port=self.port, db=self.dbname) @monitor.timer('create_transaction', rate=bigchaindb.config['statsd']['rate']) - def create_transaction(self, current_owner, new_owner, tx_input, operation, payload=None): + def create_transaction(self, *args, **kwargs): """Create a new transaction - Refer to the documentation of ``bigchaindb.util.create_tx`` + Refer to the documentation of your consensus plugin. + + Returns: + dict: newly constructed transaction. """ - return util.create_tx(current_owner, new_owner, tx_input, operation, payload) + return self.consensus.create_transaction(*args, **kwargs) - def sign_transaction(self, transaction, private_key): + def sign_transaction(self, transaction, *args, **kwargs): """Sign a transaction - Refer to the documentation of ``bigchaindb.util.sign_tx`` + Refer to the documentation of your consensus plugin. + + Returns: + dict: transaction with any signatures applied. """ - return util.sign_tx(transaction, private_key) + return self.consensus.sign_transaction(transaction, *args, **kwargs) - def verify_signature(self, signed_transaction): - """Verify the signature of a transaction. + def verify_signature(self, signed_transaction, *args, **kwargs): + """Verify the signature(s) of a transaction. - Refer to the documentation of ``bigchaindb.crypto.verify_signature`` + Refer to the documentation of your consensus plugin. + + Returns: + bool: True if the transaction's required signature data is present + and correct, False otherwise. """ - data = signed_transaction.copy() - - # if assignee field in the transaction, remove it - if 'assignee' in data: - data.pop('assignee') - - signature = data.pop('signature') - public_key_base58 = signed_transaction['transaction']['current_owner'] - public_key = crypto.PublicKey(public_key_base58) - return public_key.verify(util.serialize(data), signature) + return self.consensus.verify_signature( + signed_transaction, *args, **kwargs) @monitor.timer('write_transaction', rate=bigchaindb.config['statsd']['rate']) def write_transaction(self, signed_transaction, durability='soft'): @@ -111,7 +113,7 @@ class Bigchain(object): it has been validated by the nodes of the federation. Args: - singed_transaction (dict): transaction with the `signature` included. + signed_transaction (dict): transaction with the `signature` included. Returns: dict: database response @@ -250,11 +252,7 @@ class Bigchain(object): exception describing the reason why the transaction is invalid. """ - for plugin in self.consensus_plugins: - transaction = plugin.validate_transaction(self, transaction) - - return transaction - + return self.consensus.validate_transaction(self, transaction) def is_valid_transaction(self, transaction): """Check whether a transacion is valid or invalid. @@ -324,10 +322,8 @@ class Bigchain(object): describing the reason why the block is invalid. """ - # First run all of the plugin block validation logic - for plugin in self.consensus_plugins: - transaction = plugin.validate_block(self, block) - + # First: Run the plugin block validation logic + self.consensus.validate_block(self, block) # Finally: Tentative assumption that every blockchain will want to # validate all transactions in each block @@ -372,6 +368,8 @@ class Bigchain(object): response = r.table('bigchain').get_all(transaction_id, index='transaction_id').run(self.conn) return True if len(response.items) > 0 else False + # TODO: Unless we prescribe the signature of create_transaction, this will + # also need to be moved into the plugin API. def create_genesis_block(self): """Create the genesis block diff --git a/setup.py b/setup.py index 475cb3cc..f6fb0c43 100644 --- a/setup.py +++ b/setup.py @@ -67,8 +67,8 @@ setup( 'bigchaindb=bigchaindb.commands.bigchain:main', 'bigchaindb-benchmark=bigchaindb.commands.bigchain_benchmark:main' ], - 'bigchaindb.plugins': [ - 'base=bigchaindb.consensus.base:ConsensusRules' + 'bigchaindb.consensus': [ + 'default=bigchaindb.consensus:BaseConsensusRules' ] }, install_requires=[