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.
This commit is contained in:
Matt Smith 2016-03-08 18:33:31 -08:00
parent 14b71537d6
commit a5243e43f6
4 changed files with 159 additions and 95 deletions

View File

@ -20,6 +20,7 @@ import collections
from pkg_resources import iter_entry_points, ResolutionError from pkg_resources import iter_entry_points, ResolutionError
import bigchaindb import bigchaindb
from bigchaindb.consensus import AbstractConsensusRules
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
CONFIG_DEFAULT_PATH = os.environ.setdefault( CONFIG_DEFAULT_PATH = os.environ.setdefault(

View File

@ -19,7 +19,7 @@ class AbstractConsensusRules(metaclass=ABCMeta):
"""Validate a transaction. """Validate a transaction.
Args: Args:
bigchain (Bigchain): an instantiated bigchaindb.Bigchain object. bigchain (Bigchain): an instantiated ``bigchaindb.Bigchain`` object.
transaction (dict): transaction to validate. transaction (dict): transaction to validate.
Returns: Returns:
@ -37,7 +37,7 @@ class AbstractConsensusRules(metaclass=ABCMeta):
"""Validate a block. """Validate a block.
Args: Args:
bigchain (Bigchain): an instantiated bigchaindb.Bigchain object. bigchain (Bigchain): an instantiated ``bigchaindb.Bigchain`` object.
block (dict): block to validate. block (dict): block to validate.
Returns: Returns:
@ -48,109 +48,174 @@ class AbstractConsensusRules(metaclass=ABCMeta):
Descriptive exceptions indicating the reason the block failed. Descriptive exceptions indicating the reason the block failed.
See the `exceptions` module for bigchain-native error classes. 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. """Base consensus rules for Bigchain.
This class can be copied to write your own consensus rules! This class can be copied or overridden to write your own consensus rules!
Note: Consensus plugins will be executed in the order that they're listed in
the bigchain config file.
""" """
@classmethod @staticmethod
def validate_transaction(cls, bigchain, transaction): def validate_transaction(bigchain, transaction):
"""Validate a transaction. """Validate a transaction.
Args: Args:
bigchain (Bigchain): an instantiated bigchaindb.Bigchain object. bigchain (Bigchain): an instantiated bigchaindb.Bigchain object.
transaction (dict): transaction to validate. transaction (dict): transaction to validate.
Returns: Returns:
The transaction if the transaction is valid else it raises an The transaction if the transaction is valid else it raises an
exception describing the reason why the transaction is invalid. exception describing the reason why the transaction is invalid.
Raises: Raises:
OperationError: if the transaction operation is not supported OperationError: if the transaction operation is not supported
TransactionDoesNotExist: if the input of the transaction is not found TransactionDoesNotExist: if the input of the transaction is not found
TransactionOwnerError: if the new transaction is using an input it doesn't own TransactionOwnerError: if the new transaction is using an input it doesn't own
DoubleSpend: if the transaction is a double spend DoubleSpend: if the transaction is a double spend
InvalidHash: if the hash of the transaction is wrong InvalidHash: if the hash of the transaction is wrong
InvalidSignature: if the signature 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 # If the operation is CREATE the transaction should have no inputs and
# should be signed by a federation node # should be signed by a federation node
if transaction['transaction']['operation'] == 'CREATE': if transaction['transaction']['operation'] == 'CREATE':
if transaction['transaction']['input']: if transaction['transaction']['input']:
raise ValueError('A CREATE operation has no inputs') raise ValueError('A CREATE operation has no inputs')
if transaction['transaction']['current_owner'] not in ( if transaction['transaction']['current_owner'] not in (
bigchain.federation_nodes + [bigchain.me]): bigchain.federation_nodes + [bigchain.me]):
raise exceptions.OperationError( raise exceptions.OperationError(
'Only federation nodes can use the operation `CREATE`') 'Only federation nodes can use the operation `CREATE`')
else: else:
# check if the input exists, is owned by the current_owner # check if the input exists, is owned by the current_owner
if not transaction['transaction']['input']: if not transaction['transaction']['input']:
raise ValueError( raise ValueError(
'Only `CREATE` transactions can have null inputs') 'Only `CREATE` transactions can have null inputs')
tx_input = bigchain.get_transaction( tx_input = bigchain.get_transaction(
transaction['transaction']['input']) transaction['transaction']['input'])
if not tx_input: if not tx_input:
raise exceptions.TransactionDoesNotExist( raise exceptions.TransactionDoesNotExist(
'input `{}` does not exist in the bigchain'.format( 'input `{}` does not exist in the bigchain'.format(
transaction['transaction']['input'])) transaction['transaction']['input']))
if (tx_input['transaction']['new_owner'] != if (tx_input['transaction']['new_owner'] !=
transaction['transaction']['current_owner']): transaction['transaction']['current_owner']):
raise exceptions.TransactionOwnerError( raise exceptions.TransactionOwnerError(
'current_owner `{}` does not own the input `{}`'.format( 'current_owner `{}` does not own the input `{}`'.format(
transaction['transaction']['current_owner'], transaction['transaction']['current_owner'],
transaction['transaction']['input'])) transaction['transaction']['input']))
# check if the input was already spent by a transaction other than # check if the input was already spent by a transaction other than
# this one. # this one.
spent = bigchain.get_spent(tx_input['id']) spent = bigchain.get_spent(tx_input['id'])
if spent and spent['id'] != transaction['id']: if spent and spent['id'] != transaction['id']:
raise exceptions.DoubleSpend( raise exceptions.DoubleSpend(
'input `{}` was already spent'.format( 'input `{}` was already spent'.format(
transaction['transaction']['input'])) transaction['transaction']['input']))
# Check hash of the transaction # Check hash of the transaction
calculated_hash = hash_data(util.serialize( calculated_hash = hash_data(util.serialize(
transaction['transaction'])) transaction['transaction']))
if calculated_hash != transaction['id']: if calculated_hash != transaction['id']:
raise exceptions.InvalidHash() raise exceptions.InvalidHash()
# Check signature # Check signature
if not bigchain.verify_signature(transaction): if not util.verify_signature(transaction):
raise exceptions.InvalidSignature() raise exceptions.InvalidSignature()
return transaction return transaction
# TODO: check that the votings structure is correctly constructed # TODO: Unsure if a bigchain parameter is really necessary here?
@classmethod @staticmethod
def validate_block(cls, bigchain, block): def validate_block(bigchain, block):
"""Validate a block. """Validate a block.
Args: Args:
bigchain (Bigchain): an instantiated bigchaindb.Bigchain object. bigchain (Bigchain): an instantiated bigchaindb.Bigchain object.
block (dict): block to validate. block (dict): block to validate.
Returns: Returns:
The block if the block is valid else it raises and exception The block if the block is valid else it raises and exception
describing the reason why the block is invalid. describing the reason why the block is invalid.
Raises: 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 # Check if current hash is correct
calculated_hash = hash_data(util.serialize(block['block'])) calculated_hash = hash_data(util.serialize(block['block']))
if calculated_hash != block['id']: if calculated_hash != block['id']:
raise exceptions.InvalidHash() raise exceptions.InvalidHash()
return block 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)

View File

@ -27,7 +27,7 @@ class Bigchain(object):
def __init__(self, host=None, port=None, dbname=None, def __init__(self, host=None, port=None, dbname=None,
public_key=None, private_key=None, keyring=[], public_key=None, private_key=None, keyring=[],
consensus_plugins=['base']): consensus_plugin=None):
"""Initialize the Bigchain instance """Initialize the Bigchain instance
There are three ways in which the Bigchain instance can get its parameters. 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 = public_key or bigchaindb.config['keypair']['public']
self.me_private = private_key or bigchaindb.config['keypair']['private'] self.me_private = private_key or bigchaindb.config['keypair']['private']
self.federation_nodes = keyring or bigchaindb.config['keyring'] 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: if not self.me or not self.me_private:
raise exceptions.KeypairNotFoundException() raise exceptions.KeypairNotFoundException()
@ -70,38 +70,40 @@ class Bigchain(object):
return r.connect(host=self.host, port=self.port, db=self.dbname) return r.connect(host=self.host, port=self.port, db=self.dbname)
@monitor.timer('create_transaction', rate=bigchaindb.config['statsd']['rate']) @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 """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 """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): def verify_signature(self, signed_transaction, *args, **kwargs):
"""Verify the signature of a transaction. """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() return self.consensus.verify_signature(
signed_transaction, *args, **kwargs)
# 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)
@monitor.timer('write_transaction', rate=bigchaindb.config['statsd']['rate']) @monitor.timer('write_transaction', rate=bigchaindb.config['statsd']['rate'])
def write_transaction(self, signed_transaction, durability='soft'): 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. it has been validated by the nodes of the federation.
Args: Args:
singed_transaction (dict): transaction with the `signature` included. signed_transaction (dict): transaction with the `signature` included.
Returns: Returns:
dict: database response dict: database response
@ -250,11 +252,7 @@ class Bigchain(object):
exception describing the reason why the transaction is invalid. exception describing the reason why the transaction is invalid.
""" """
for plugin in self.consensus_plugins: return self.consensus.validate_transaction(self, transaction)
transaction = plugin.validate_transaction(self, transaction)
return transaction
def is_valid_transaction(self, transaction): def is_valid_transaction(self, transaction):
"""Check whether a transacion is valid or invalid. """Check whether a transacion is valid or invalid.
@ -324,10 +322,8 @@ class Bigchain(object):
describing the reason why the block is invalid. describing the reason why the block is invalid.
""" """
# First run all of the plugin block validation logic # First: Run the plugin block validation logic
for plugin in self.consensus_plugins: self.consensus.validate_block(self, block)
transaction = plugin.validate_block(self, block)
# Finally: Tentative assumption that every blockchain will want to # Finally: Tentative assumption that every blockchain will want to
# validate all transactions in each block # 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) response = r.table('bigchain').get_all(transaction_id, index='transaction_id').run(self.conn)
return True if len(response.items) > 0 else False 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): def create_genesis_block(self):
"""Create the genesis block """Create the genesis block

View File

@ -67,8 +67,8 @@ setup(
'bigchaindb=bigchaindb.commands.bigchain:main', 'bigchaindb=bigchaindb.commands.bigchain:main',
'bigchaindb-benchmark=bigchaindb.commands.bigchain_benchmark:main' 'bigchaindb-benchmark=bigchaindb.commands.bigchain_benchmark:main'
], ],
'bigchaindb.plugins': [ 'bigchaindb.consensus': [
'base=bigchaindb.consensus.base:ConsensusRules' 'default=bigchaindb.consensus:BaseConsensusRules'
] ]
}, },
install_requires=[ install_requires=[