diff --git a/bigchaindb/__init__.py b/bigchaindb/__init__.py index 05bf85a0..2999b42e 100644 --- a/bigchaindb/__init__.py +++ b/bigchaindb/__init__.py @@ -87,3 +87,10 @@ config = { # the user wants to reconfigure the node. Check ``bigchaindb.config_utils`` # for more info. _config = copy.deepcopy(config) +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/crypto.py b/bigchaindb/common/crypto.py index 99663fe9..58b3faa0 100644 --- a/bigchaindb/common/crypto.py +++ b/bigchaindb/common/crypto.py @@ -30,3 +30,17 @@ def generate_key_pair(): PrivateKey = crypto.Ed25519SigningKey PublicKey = crypto.Ed25519VerifyingKey + + +def key_pair_from_ed25519_key(hex_private_key): + """Generate base58 encode public-private key pair from a hex encoded private key""" + priv_key = crypto.Ed25519SigningKey(bytes.fromhex(hex_private_key)[:32], encoding='bytes') + public_key = priv_key.get_verifying_key() + return CryptoKeypair(private_key=priv_key.encode(encoding='base58').decode('utf-8'), + public_key=public_key.encode(encoding='base58').decode('utf-8')) + + +def public_key_from_ed25519_key(hex_public_key): + """Generate base58 public key from hex encoded public key""" + public_key = crypto.Ed25519VerifyingKey(bytes.fromhex(hex_public_key), encoding='bytes') + return public_key.encode(encoding='base58').decode('utf-8') diff --git a/bigchaindb/common/exceptions.py b/bigchaindb/common/exceptions.py index 735d8476..0c145d13 100644 --- a/bigchaindb/common/exceptions.py +++ b/bigchaindb/common/exceptions.py @@ -102,3 +102,19 @@ class GenesisBlockAlreadyExistsError(ValidationError): class MultipleValidatorOperationError(ValidationError): """Raised when a validator update pending but new request is submited""" + + +class MultipleInputsError(ValidationError): + """Raised if there were multiple inputs when only one was expected""" + + +class InvalidProposer(ValidationError): + """Raised if the public key is not a part of the validator set""" + + +class UnequalValidatorSet(ValidationError): + """Raised if the validator sets differ""" + + +class InvalidPowerChange(ValidationError): + """Raised if proposed power change in validator set is >=1/3 total power""" diff --git a/bigchaindb/common/schema/__init__.py b/bigchaindb/common/schema/__init__.py index c3d1d64d..cd2c55f5 100644 --- a/bigchaindb/common/schema/__init__.py +++ b/bigchaindb/common/schema/__init__.py @@ -13,9 +13,9 @@ from bigchaindb.common.exceptions import SchemaValidationError logger = logging.getLogger(__name__) -def _load_schema(name): +def _load_schema(name, path=__file__): """Load a schema from disk""" - path = os.path.join(os.path.dirname(__file__), name + '.yaml') + path = os.path.join(os.path.dirname(path), name + '.yaml') with open(path) as handle: schema = yaml.safe_load(handle) fast_schema = rapidjson_schema.loads(rapidjson.dumps(schema)) diff --git a/bigchaindb/common/schema/transaction_v2.0.yaml b/bigchaindb/common/schema/transaction_v2.0.yaml index 0a22b810..c727b278 100644 --- a/bigchaindb/common/schema/transaction_v2.0.yaml +++ b/bigchaindb/common/schema/transaction_v2.0.yaml @@ -58,6 +58,7 @@ definitions: enum: - CREATE - TRANSFER + - VALIDATOR_ELECTION asset: type: object additionalProperties: false diff --git a/bigchaindb/common/transaction.py b/bigchaindb/common/transaction.py index 59c58264..7098d366 100644 --- a/bigchaindb/common/transaction.py +++ b/bigchaindb/common/transaction.py @@ -515,7 +515,7 @@ class Transaction(object): version (string): Defines the version number of a Transaction. hash_id (string): Hash id of the transaction. """ - if operation not in Transaction.ALLOWED_OPERATIONS: + if operation not in self.ALLOWED_OPERATIONS: allowed_ops = ', '.join(self.__class__.ALLOWED_OPERATIONS) raise ValueError('`operation` must be one of {}' .format(allowed_ops)) @@ -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 @@ -939,7 +947,7 @@ class Transaction(object): Returns: bool: If all Inputs are valid. """ - if self.operation == Transaction.CREATE: + if self.operation == self.CREATE: # NOTE: Since in the case of a `CREATE`-transaction we do not have # to check for outputs, we're just submitting dummy # values to the actual method. This simplifies it's logic @@ -986,8 +994,7 @@ class Transaction(object): return all(validate(i, cond) for i, cond in enumerate(output_condition_uris)) - @staticmethod - def _input_valid(input_, operation, message, output_condition_uri=None): + def _input_valid(self, input_, operation, message, output_condition_uri=None): """Validates a single Input against a single Output. Note: @@ -1012,7 +1019,7 @@ class Transaction(object): ParsingError, ASN1DecodeError, ASN1EncodeError): return False - if operation == Transaction.CREATE: + if operation == self.CREATE: # NOTE: In the case of a `CREATE` transaction, the # output is always valid. output_valid = True @@ -1151,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: @@ -1160,7 +1167,78 @@ 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, tx['metadata'], tx['version'], hash_id=tx['id']) + + @classmethod + def from_db(cls, bigchain, tx_dict_list): + """Helper method that reconstructs a transaction dict that was returned + from the database. It checks what asset_id to retrieve, retrieves the + asset from the asset table and reconstructs the transaction. + + Args: + bigchain (:class:`~bigchaindb.tendermint.BigchainDB`): An instance + of BigchainDB used to perform database queries. + tx_dict_list (:list:`dict` or :obj:`dict`): The transaction dict or + list of transaction dict as returned from the database. + + Returns: + :class:`~Transaction` + + """ + return_list = True + if isinstance(tx_dict_list, dict): + tx_dict_list = [tx_dict_list] + return_list = False + + tx_map = {} + tx_ids = [] + for tx in tx_dict_list: + tx.update({'metadata': None}) + tx_map[tx['id']] = tx + tx_ids.append(tx['id']) + + assets = list(bigchain.get_assets(tx_ids)) + for asset in assets: + if asset is not None: + tx = tx_map[asset['id']] + del asset['id'] + tx['asset'] = asset + + tx_ids = list(tx_map.keys()) + metadata_list = list(bigchain.get_metadata(tx_ids)) + for metadata in metadata_list: + tx = tx_map[metadata['id']] + tx.update({'metadata': metadata.get('metadata')}) + + if return_list: + tx_list = [] + for tx_id, tx in tx_map.items(): + tx_list.append(cls.from_dict(tx)) + return tx_list + 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""" + + create_txn_class = Transaction.type_registry.get(Transaction.CREATE) + return Transaction.type_registry.get(operation, create_txn_class) + + @classmethod + def validate_schema(cls, tx): + pass diff --git a/bigchaindb/lib.py b/bigchaindb/lib.py index 80e93bf3..b0545d83 100644 --- a/bigchaindb/lib.py +++ b/bigchaindb/lib.py @@ -149,10 +149,10 @@ class BigchainDB(object): txns = [] assets = [] txn_metadatas = [] - for transaction in transactions: + for transaction_obj in transactions: # self.update_utxoset(transaction) - transaction = transaction.to_dict() - if transaction['operation'] == 'CREATE': + transaction = transaction_obj.to_dict() + if transaction['operation'] == transaction_obj.CREATE: asset = transaction.pop('asset') asset['id'] = transaction['id'] assets.append(asset) @@ -391,7 +391,7 @@ class BigchainDB(object): # CLEANUP: The conditional below checks for transaction in dict format. # It would be better to only have a single format for the transaction # throught the code base. - if not isinstance(transaction, Transaction): + if isinstance(transaction, dict): try: transaction = Transaction.from_dict(tx) except SchemaValidationError as e: diff --git a/bigchaindb/models.py b/bigchaindb/models.py index 62c04517..1ab0dd0e 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,62 +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) - - @classmethod - def from_db(cls, bigchain, tx_dict_list): - """Helper method that reconstructs a transaction dict that was returned - from the database. It checks what asset_id to retrieve, retrieves the - asset from the asset table and reconstructs the transaction. - - Args: - bigchain (:class:`~bigchaindb.BigchainDB`): An instance - of BigchainDB used to perform database queries. - tx_dict_list (:list:`dict` or :obj:`dict`): The transaction dict or - list of transaction dict as returned from the database. - - Returns: - :class:`~Transaction` - - """ - return_list = True - if isinstance(tx_dict_list, dict): - tx_dict_list = [tx_dict_list] - return_list = False - - tx_map = {} - tx_ids = [] - for tx in tx_dict_list: - tx.update({'metadata': None}) - tx_map[tx['id']] = tx - if tx['operation'] == Transaction.CREATE: - tx_ids.append(tx['id']) - - assets = list(bigchain.get_assets(tx_ids)) - for asset in assets: - tx = tx_map[asset['id']] - del asset['id'] - tx.update({'asset': asset}) - - tx_ids = list(tx_map.keys()) - metadata_list = list(bigchain.get_metadata(tx_ids)) - for metadata in metadata_list: - tx = tx_map[metadata['id']] - tx.update({'metadata': metadata.get('metadata')}) - - if return_list: - tx_list = [] - for tx_id, tx in tx_map.items(): - tx_list.append(cls.from_dict(tx)) - return tx_list - else: - tx = list(tx_map.values())[0] - return cls.from_dict(tx) class FastTransaction: diff --git a/bigchaindb/tendermint_utils.py b/bigchaindb/tendermint_utils.py index e6efb463..3299456a 100644 --- a/bigchaindb/tendermint_utils.py +++ b/bigchaindb/tendermint_utils.py @@ -75,12 +75,20 @@ def public_key64_to_address(base64_public_key): def public_key_from_base64(base64_public_key): - return base64.b64decode(base64_public_key).hex().upper() + return key_from_base64(base64_public_key) + + +def key_from_base64(base64_key): + return base64.b64decode(base64_key).hex().upper() def public_key_to_base64(ed25519_public_key): - ed25519_public_key = bytes.fromhex(ed25519_public_key) - return base64.b64encode(ed25519_public_key).decode('utf-8') + return key_to_base64(ed25519_public_key) + + +def key_to_base64(ed25519_key): + ed25519_key = bytes.fromhex(ed25519_key) + return base64.b64encode(ed25519_key).decode('utf-8') def amino_encoded_public_key(ed25519_public_key): diff --git a/bigchaindb/upsert_validator/__init__.py b/bigchaindb/upsert_validator/__init__.py new file mode 100644 index 00000000..2d415220 --- /dev/null +++ b/bigchaindb/upsert_validator/__init__.py @@ -0,0 +1,2 @@ + +from bigchaindb.upsert_validator.validator_election import ValidatorElection # noqa diff --git a/bigchaindb/upsert_validator/transaction_validator_election_v2.0.yaml b/bigchaindb/upsert_validator/transaction_validator_election_v2.0.yaml new file mode 100644 index 00000000..ce107f58 --- /dev/null +++ b/bigchaindb/upsert_validator/transaction_validator_election_v2.0.yaml @@ -0,0 +1,48 @@ +--- +"$schema": "http://json-schema.org/draft-04/schema#" +type: object +title: Validator Election Schema - Propose a change to validator set +required: +- operation +- asset +- outputs +properties: + operation: + type: string + value: "VALIDATOR_ELECTION" + asset: + additionalProperties: false + properties: + data: + additionalProperties: false + properties: + node_id: + type: string + public_key: + type: string + power: + "$ref": "#/definitions/positiveInteger" + required: + - node_id + - public_key + - power + required: + - data + outputs: + type: array + items: + "$ref": "#/definitions/output" +definitions: + output: + type: object + properties: + condition: + type: object + required: + - uri + properties: + uri: + type: string + pattern: "^ni:///sha-256;([a-zA-Z0-9_-]{0,86})[?]\ + (fpt=ed25519-sha-256(&)?|cost=[0-9]+(&)?|\ + subtypes=ed25519-sha-256(&)?){2,3}$" diff --git a/bigchaindb/upsert_validator/validator_election.py b/bigchaindb/upsert_validator/validator_election.py new file mode 100644 index 00000000..a1fd6996 --- /dev/null +++ b/bigchaindb/upsert_validator/validator_election.py @@ -0,0 +1,148 @@ +from bigchaindb.common.exceptions import (InvalidSignature, + MultipleInputsError, + InvalidProposer, + UnequalValidatorSet, + InvalidPowerChange, + 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 +from bigchaindb.common.schema import (_load_schema, + _validate_schema, + TX_SCHEMA_VERSION, + TX_SCHEMA_COMMON, + TX_SCHEMA_CREATE) + + +_, TX_SCHEMA_VALIDATOR_ELECTION = _load_schema('transaction_validator_election_' + + TX_SCHEMA_VERSION, __file__) + + +class ValidatorElection(Transaction): + + VALIDATOR_ELECTION = 'VALIDATOR_ELECTION' + # NOTE: this transaction class extends create so the operation inheritence is achieved + # by renaming CREATE to VALIDATOR_ELECTION + CREATE = VALIDATOR_ELECTION + ALLOWED_OPERATIONS = (VALIDATOR_ELECTION,) + + 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) + + @classmethod + def current_validators(cls, bigchain): + """Return a dictionary of validators with key as `public_key` and + value as the `voting_power` + """ + + validators = {} + for validator in bigchain.get_validators(): + # NOTE: we assume that Tendermint encodes public key in base64 + public_key = public_key_from_ed25519_key(key_from_base64(validator['pub_key']['value'])) + validators[public_key] = validator['voting_power'] + + return validators + + @classmethod + def recipients(cls, bigchain): + """Convert validator dictionary to a recipient list for `Transaction`""" + + recipients = [] + for public_key, voting_power in cls.current_validators(bigchain).items(): + recipients.append(([public_key], voting_power)) + + return recipients + + @classmethod + def is_same_topology(cls, current_topology, election_topology): + voters = {} + for voter in election_topology: + if len(voter.public_keys) > 1: + return False + + [public_key] = voter.public_keys + voting_power = voter.amount + voters[public_key] = voting_power + + # Check whether the voters and their votes is same to that of the + # validators and their voting power in the network + return (current_topology == voters) + + def validate(self, bigchain, current_transactions=[]): + """Validate election transaction + For more details refer BEP-21: https://github.com/bigchaindb/BEPs/tree/master/21 + + NOTE: + * A valid election is initiated by an existing validator. + + * A valid election is one where voters are validators and votes are + alloacted according to the voting power of each validator node. + + Args: + bigchain (BigchainDB): an instantiated bigchaindb.lib.BigchainDB object. + + Returns: + `True` if the election is valid + + Raises: + ValidationError: If the election is invalid + """ + input_conditions = [] + + duplicates = any(txn for txn in current_transactions if txn.id == self.id) + if bigchain.get_transaction(self.id) or duplicates: + raise DuplicateTransaction('transaction `{}` already exists' + .format(self.id)) + + if not self.inputs_valid(input_conditions): + raise InvalidSignature('Transaction signature is invalid.') + + current_validators = self.current_validators(bigchain) + + # NOTE: Proposer should be a single node + if len(self.inputs) != 1 or len(self.inputs[0].owners_before) != 1: + raise MultipleInputsError('`tx_signers` must be a list instance of length one') + + # NOTE: change more than 1/3 of the current power is not allowed + if self.asset['data']['power'] >= (1/3)*sum(current_validators.values()): + raise InvalidPowerChange('`power` change must be less than 1/3 of total power') + + # NOTE: Check if the proposer is a validator. + [election_initiator_node_pub_key] = self.inputs[0].owners_before + if election_initiator_node_pub_key not in current_validators.keys(): + raise InvalidProposer('Public key is not a part of the validator set') + + # NOTE: Check if all validators have been assigned votes equal to their voting power + if not self.is_same_topology(current_validators, self.outputs): + raise UnequalValidatorSet('Validator set much be exactly same to the outputs of election') + + return True + + @classmethod + def generate(cls, initiator, voters, election_data, metadata=None): + (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 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) + + @classmethod + def create(cls, tx_signers, recipients, metadata=None, asset=None): + raise NotImplementedError + + @classmethod + def transfer(cls, tx_signers, recipients, metadata=None, asset=None): + raise NotImplementedError diff --git a/bigchaindb/web/views/transactions.py b/bigchaindb/web/views/transactions.py index 61a592ee..299ace5a 100644 --- a/bigchaindb/web/views/transactions.py +++ b/bigchaindb/web/views/transactions.py @@ -8,9 +8,10 @@ from flask import current_app, request, jsonify from flask_restful import Resource, reqparse from bigchaindb.common.exceptions import SchemaValidationError, ValidationError -from bigchaindb.models import Transaction from bigchaindb.web.views.base import make_error from bigchaindb.web.views import parameters +from bigchaindb.models import Transaction + logger = logging.getLogger(__name__) diff --git a/tests/conftest.py b/tests/conftest.py index f4c05197..47f8ce30 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -17,8 +17,12 @@ from pymongo import MongoClient from bigchaindb.common import crypto from bigchaindb.log import setup_logging +from bigchaindb.tendermint_utils import key_from_base64 +from bigchaindb.common.crypto import (key_pair_from_ed25519_key, + public_key_from_ed25519_key) from bigchaindb.lib import Block + TEST_DB_NAME = 'bigchain_test' USER2_SK, USER2_PK = crypto.generate_key_pair() @@ -615,3 +619,41 @@ def utxoset(dummy_unspent_outputs, utxo_collection): assert res.acknowledged assert len(res.inserted_ids) == 3 return dummy_unspent_outputs, utxo_collection + + +@pytest.fixture +def network_validators(node_keys): + validator_pub_power = {} + voting_power = [8, 10, 7, 9] + for pub, priv in node_keys.items(): + validator_pub_power[pub] = voting_power.pop() + + return validator_pub_power + + +@pytest.fixture +def network_validators58(network_validators): + network_validators_base58 = {} + for p, v in network_validators.items(): + p = public_key_from_ed25519_key(key_from_base64(p)) + network_validators_base58[p] = v + + return network_validators_base58 + + +@pytest.fixture +def node_key(node_keys): + (pub, priv) = list(node_keys.items())[0] + return key_pair_from_ed25519_key(key_from_base64(priv)) + + +@pytest.fixture(scope='session') +def node_keys(): + return {'zL/DasvKulXZzhSNFwx4cLRXKkSM9GPK7Y0nZ4FEylM=': + 'cM5oW4J0zmUSZ/+QRoRlincvgCwR0pEjFoY//ZnnjD3Mv8Nqy8q6VdnOFI0XDHhwtFcqRIz0Y8rtjSdngUTKUw==', + 'GIijU7GBcVyiVUcB0GwWZbxCxdk2xV6pxdvL24s/AqM=': + 'mdz7IjP6mGXs6+ebgGJkn7kTXByUeeGhV+9aVthLuEAYiKNTsYFxXKJVRwHQbBZlvELF2TbFXqnF28vbiz8Cow==', + 'JbfwrLvCVIwOPm8tj8936ki7IYbmGHjPiKb6nAZegRA=': + '83VINXdj2ynOHuhvSZz5tGuOE5oYzIi0mEximkX1KYMlt/Csu8JUjA4+by2Pz3fqSLshhuYYeM+IpvqcBl6BEA==', + 'PecJ58SaNRsWJZodDmqjpCWqG6btdwXFHLyE40RYlYM=': + 'uz8bYgoL4rHErWT1gjjrnA+W7bgD/uDQWSRKDmC8otc95wnnxJo1GxYlmh0OaqOkJaobpu13BcUcvITjRFiVgw=='} diff --git a/tests/upsert_validator/__init__.py b/tests/upsert_validator/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/upsert_validator/conftest.py b/tests/upsert_validator/conftest.py new file mode 100644 index 00000000..9f3d37b9 --- /dev/null +++ b/tests/upsert_validator/conftest.py @@ -0,0 +1,32 @@ +import pytest + + +@pytest.fixture +def b_mock(b, network_validators): + b.get_validators = mock_get_validators(network_validators) + + return b + + +@pytest.fixture +def new_validator(): + public_key = '1718D2DBFF00158A0852A17A01C78F4DCF3BA8E4FB7B8586807FAC182A535034' + power = 1 + node_id = 'fake_node_id' + + return {'public_key': public_key, + 'power': power, + 'node_id': node_id} + + +def mock_get_validators(network_validators): + def validator_set(): + validators = [] + for public_key, power in network_validators.items(): + validators.append({ + 'pub_key': {'type': 'AC26791624DE60', 'value': public_key}, + 'voting_power': power + }) + return validators + + return validator_set diff --git a/tests/upsert_validator/test_validator_election.py b/tests/upsert_validator/test_validator_election.py new file mode 100644 index 00000000..e67b3e8d --- /dev/null +++ b/tests/upsert_validator/test_validator_election.py @@ -0,0 +1,93 @@ +import pytest + +from bigchaindb.upsert_validator import ValidatorElection +from bigchaindb.common.exceptions import (DuplicateTransaction, + UnequalValidatorSet, + InvalidProposer, + MultipleInputsError, + InvalidPowerChange) + +pytestmark = [pytest.mark.tendermint, pytest.mark.bdb] + + +def test_upsert_validator_valid_election(b_mock, new_validator, node_key): + voters = ValidatorElection.recipients(b_mock) + election = ValidatorElection.generate([node_key.public_key], + voters, + new_validator, None).sign([node_key.private_key]) + assert election.validate(b_mock) + + +def test_upsert_validator_invalid_power_election(b_mock, new_validator, node_key): + voters = ValidatorElection.recipients(b_mock) + new_validator['power'] = 30 + + election = ValidatorElection.generate([node_key.public_key], + voters, + new_validator, None).sign([node_key.private_key]) + with pytest.raises(InvalidPowerChange): + election.validate(b_mock) + + +def test_upsert_validator_invalid_proposed_election(b_mock, new_validator, node_key): + from bigchaindb.common.crypto import generate_key_pair + + alice = generate_key_pair() + voters = ValidatorElection.recipients(b_mock) + election = ValidatorElection.generate([alice.public_key], + voters, + new_validator, None).sign([alice.private_key]) + with pytest.raises(InvalidProposer): + election.validate(b_mock) + + +def test_upsert_validator_invalid_inputs_election(b_mock, new_validator, node_key): + from bigchaindb.common.crypto import generate_key_pair + + alice = generate_key_pair() + voters = ValidatorElection.recipients(b_mock) + election = ValidatorElection.generate([node_key.public_key, alice.public_key], + voters, + new_validator, None).sign([node_key.private_key, alice.private_key]) + with pytest.raises(MultipleInputsError): + election.validate(b_mock) + + +def test_upsert_validator_invalid_election(b_mock, new_validator, node_key): + voters = ValidatorElection.recipients(b_mock) + valid_election = ValidatorElection.generate([node_key.public_key], + voters, + new_validator, None).sign([node_key.private_key]) + duplicate_election = ValidatorElection.generate([node_key.public_key], + voters, + new_validator, None).sign([node_key.private_key]) + + with pytest.raises(DuplicateTransaction): + valid_election.validate(b_mock, [duplicate_election]) + + b_mock.store_bulk_transactions([valid_election]) + + with pytest.raises(DuplicateTransaction): + duplicate_election.validate(b_mock) + + # Try creating an election with incomplete voter set + invalid_election = ValidatorElection.generate([node_key.public_key], + voters[1:], + new_validator, None).sign([node_key.private_key]) + + with pytest.raises(UnequalValidatorSet): + invalid_election.validate(b_mock) + + recipients = ValidatorElection.recipients(b_mock) + altered_recipients = [] + for r in recipients: + ([r_public_key], voting_power) = r + altered_recipients.append(([r_public_key], voting_power - 1)) + + # Create a transaction which doesn't enfore the network power + tx_election = ValidatorElection.generate([node_key.public_key], + altered_recipients, + new_validator, None).sign([node_key.private_key]) + + with pytest.raises(UnequalValidatorSet): + tx_election.validate(b_mock)