From e0d09198d923e5b5a0819e7ee0600e3619323d65 Mon Sep 17 00:00:00 2001 From: Lorenz Herzberger Date: Wed, 28 Sep 2022 20:55:53 +0200 Subject: [PATCH] moved election validation to planetmint Signed-off-by: Lorenz Herzberger --- planetmint/commands/planetmint.py | 2 +- planetmint/lib.py | 94 ++++++++++++++++++- .../elections/chain_migration_election.py | 8 +- .../transactions/types/elections/election.py | 89 ++++-------------- .../types/elections/validator_election.py | 19 +--- tests/migrations/test_migration_election.py | 2 +- .../test_validator_election.py | 16 ++-- 7 files changed, 125 insertions(+), 105 deletions(-) diff --git a/planetmint/commands/planetmint.py b/planetmint/commands/planetmint.py index 00eea53..df9c195 100644 --- a/planetmint/commands/planetmint.py +++ b/planetmint/commands/planetmint.py @@ -121,7 +121,7 @@ def create_new_election(sk, planet, election_class, data): key = load_node_key(sk) voters = election_class.recipients(planet) election = election_class.generate([key.public_key], voters, data, None).sign([key.private_key]) - election.validate(planet) + planet.validate_election(election) except ValidationError as e: logger.error(e) return False diff --git a/planetmint/lib.py b/planetmint/lib.py index 1b0094f..6d89bd1 100644 --- a/planetmint/lib.py +++ b/planetmint/lib.py @@ -20,14 +20,29 @@ import requests import planetmint from planetmint.config import Config from planetmint import backend, config_utils, fastquery -from planetmint.transactions.common.transaction import Transaction -from planetmint.transactions.common.exceptions import DuplicateTransaction, InvalidSignature, SchemaValidationError, ValidationError, DoubleSpend, AmountError, InputDoesNotExist, AssetIdMismatch +from planetmint.transactions.common.transaction import VALIDATOR_ELECTION, Transaction +from planetmint.transactions.common.exceptions import ( + DuplicateTransaction, + InvalidSignature, + SchemaValidationError, + ValidationError, + DoubleSpend, + AmountError, + InputDoesNotExist, + AssetIdMismatch, + InvalidProposer, + UnequalValidatorSet, + DuplicateTransaction, + MultipleInputsError, + InvalidPowerChange +) +from planetmint.transactions.common.crypto import public_key_from_ed25519_key from planetmint.transactions.common.transaction_mode_types import ( BROADCAST_TX_COMMIT, BROADCAST_TX_ASYNC, BROADCAST_TX_SYNC, ) -from planetmint.tendermint_utils import encode_transaction, merkleroot +from planetmint.tendermint_utils import encode_transaction, merkleroot, key_from_base64 from planetmint import exceptions as core_exceptions from planetmint.validation import BaseValidationRules @@ -600,6 +615,79 @@ class Planetmint(object): tx = list(tx_map.values())[0] return Transaction.from_dict(tx) + # NOTE: moved here from Election needs to be placed somewhere else + def get_validators_dict(self, height=None): + """Return a dictionary of validators with key as `public_key` and + value as the `voting_power` + """ + validators = {} + for validator in self.get_validators(height): + # NOTE: we assume that Tendermint encodes public key in base64 + public_key = public_key_from_ed25519_key(key_from_base64(validator["public_key"]["value"])) + validators[public_key] = validator["voting_power"] + return validators + + def validate_election(self, transaction, current_transactions=[]): # TODO: move somewhere else + """Validate election transaction + + NOTE: + * A valid election is initiated by an existing validator. + + * A valid election is one where voters are validators and votes are + allocated according to the voting power of each validator node. + + Args: + :param planet: (Planetmint) an instantiated planetmint.lib.Planetmint object. + :param current_transactions: (list) A list of transactions to be validated along with the election + + Returns: + Election: a Election object or an object of the derived Election subclass. + + Raises: + ValidationError: If the election is invalid + """ + input_conditions = [] + + duplicates = any(txn for txn in current_transactions if txn.id == transaction.id) + if self.is_committed(transaction.id) or duplicates: + raise DuplicateTransaction("transaction `{}` already exists".format(transaction.id)) + + if not transaction.inputs_valid(input_conditions): + raise InvalidSignature("Transaction signature is invalid.") + + current_validators = self.get_validators_dict() + + # NOTE: Proposer should be a single node + if len(transaction.inputs) != 1 or len(transaction.inputs[0].owners_before) != 1: + raise MultipleInputsError("`tx_signers` must be a list instance of length one") + + # NOTE: Check if the proposer is a validator. + [election_initiator_node_pub_key] = transaction.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 transaction.is_same_topology(current_validators, transaction.outputs): + raise UnequalValidatorSet("Validator set much be exactly same to the outputs of election") + + if transaction.operation == VALIDATOR_ELECTION: + self.validate_validator_election(transaction, current_transactions) + + return transaction + + + def validate_validator_election(self, transaction, current_transactions=[]): # TODO: move somewhere else + """For more details refer BEP-21: https://github.com/planetmint/BEPs/tree/master/21""" + + current_validators = self.get_validators_dict() + + # super(ValidatorElection, self).validate(planet, current_transactions=current_transactions) + + # NOTE: change more than 1/3 of the current power is not allowed + if transaction.asset["data"]["power"] >= (1 / 3) * sum(current_validators.values()): + raise InvalidPowerChange("`power` change must be less than 1/3 of total power") + + return transaction Block = namedtuple("Block", ("app_hash", "height", "transactions")) diff --git a/planetmint/transactions/types/elections/chain_migration_election.py b/planetmint/transactions/types/elections/chain_migration_election.py index 9c7da3e..978cc6c 100644 --- a/planetmint/transactions/types/elections/chain_migration_election.py +++ b/planetmint/transactions/types/elections/chain_migration_election.py @@ -12,7 +12,7 @@ class ChainMigrationElection(Election): ALLOWED_OPERATIONS = (OPERATION,) TX_SCHEMA_CUSTOM = TX_SCHEMA_CHAIN_MIGRATION_ELECTION - def has_concluded(self, planetmint, *args, **kwargs): + def has_concluded(self, planetmint, *args, **kwargs): # TODO: move somewhere else chain = planetmint.get_latest_abci_chain() if chain is not None and not chain["is_synced"]: # do not conclude the migration election if @@ -21,10 +21,10 @@ class ChainMigrationElection(Election): return super().has_concluded(planetmint, *args, **kwargs) - def on_approval(self, planet, *args, **kwargs): + def on_approval(self, planet, *args, **kwargs): # TODO: move somewhere else planet.migrate_abci_chain() - def show_election(self, planet): + def show_election(self, planet): # TODO: move somewhere else output = super().show_election(planet) chain = planet.get_latest_abci_chain() if chain is None or chain["is_synced"]: @@ -46,5 +46,5 @@ class ChainMigrationElection(Election): output += f"\nvalidators={json.dumps(validators, indent=4)}" return output - def on_rollback(self, planet, new_height): + def on_rollback(self, planet, new_height): # TODO: move somewhere else planet.delete_abci_chain(new_height) diff --git a/planetmint/transactions/types/elections/election.py b/planetmint/transactions/types/elections/election.py index e327d85..25bafe0 100644 --- a/planetmint/transactions/types/elections/election.py +++ b/planetmint/transactions/types/elections/election.py @@ -9,8 +9,6 @@ from uuid import uuid4 from typing import Optional from planetmint import backend -from planetmint.transactions.types.assets.create import Create -from planetmint.transactions.types.assets.transfer import Transfer from planetmint.transactions.types.elections.vote import Vote from planetmint.transactions.common.exceptions import ( InvalidSignature, @@ -44,7 +42,7 @@ class Election(Transaction): ELECTION_THRESHOLD = 2 / 3 @classmethod - def get_validator_change(cls, planet): + def get_validator_change(cls, planet): # TODO: move somewhere else """Return the validator set from the most recent approved block :return: { @@ -58,7 +56,7 @@ class Election(Transaction): return planet.get_validator_change(latest_block["height"]) @classmethod - def get_validators(cls, planet, height=None): + def get_validators(cls, planet, height=None): # TODO: move somewhere else """Return a dictionary of validators with key as `public_key` and value as the `voting_power` """ @@ -71,7 +69,7 @@ class Election(Transaction): return validators @classmethod - def recipients(cls, planet): + def recipients(cls, planet): # TODO: move somewhere else """Convert validator dictionary to a recipient list for `Transaction`""" recipients = [] @@ -81,7 +79,7 @@ class Election(Transaction): return recipients @classmethod - def is_same_topology(cls, current_topology, election_topology): + def is_same_topology(cls, current_topology, election_topology): # TODO: move somewhere else voters = {} for voter in election_topology: if len(voter.public_keys) > 1: @@ -114,51 +112,6 @@ class Election(Transaction): return True - def validate(self, planet, current_transactions=[]): - """Validate election transaction - - NOTE: - * A valid election is initiated by an existing validator. - - * A valid election is one where voters are validators and votes are - allocated according to the voting power of each validator node. - - Args: - :param planet: (Planetmint) an instantiated planetmint.lib.Planetmint object. - :param current_transactions: (list) A list of transactions to be validated along with the election - - Returns: - Election: a Election object or an object of the derived Election subclass. - - Raises: - ValidationError: If the election is invalid - """ - input_conditions = [] - - duplicates = any(txn for txn in current_transactions if txn.id == self.id) - if planet.is_committed(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.get_validators(planet) - - # 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: 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 self - @classmethod def generate(cls, initiator, voters, election_data, metadata=None): # Break symmetry in case we need to call an election with the same properties twice @@ -181,19 +134,11 @@ class Election(Transaction): _validate_schema(cls.TX_SCHEMA_CUSTOM, tx) @classmethod - def create(cls, tx_signers, recipients, metadata=None, asset=None): - Create.generate(tx_signers, recipients, metadata=None, asset=None) - - @classmethod - def transfer(cls, tx_signers, recipients, metadata=None, asset=None): - Transfer.generate(tx_signers, recipients, metadata=None, asset=None) - - @classmethod - def to_public_key(cls, election_id): + def to_public_key(cls, election_id): # TODO: move somewhere else return base58.b58encode(bytes.fromhex(election_id)).decode() @classmethod - def count_votes(cls, election_pk, transactions, getter=getattr): + def count_votes(cls, election_pk, transactions, getter=getattr): # TODO: move somewhere else votes = 0 for txn in transactions: if getter(txn, "operation") == Vote.OPERATION: @@ -205,13 +150,13 @@ class Election(Transaction): votes = votes + int(getter(output, "amount")) return votes - def get_commited_votes(self, planet, election_pk=None): + def get_commited_votes(self, planet, election_pk=None): # TODO: move somewhere else if election_pk is None: election_pk = self.to_public_key(self.id) txns = list(backend.query.get_asset_tokens_for_public_key(planet.connection, self.id, election_pk)) return self.count_votes(election_pk, txns, dict.get) - def has_concluded(self, planet, current_votes=[]): + def has_concluded(self, planet, current_votes=[]): # TODO: move somewhere else """Check if the election can be concluded or not. * Elections can only be concluded if the validator set has not changed @@ -233,14 +178,14 @@ class Election(Transaction): return False - def get_status(self, planet): + def get_status(self, planet): # TODO: move somewhere else election = self.get_election(self.id, planet) if election and election["is_concluded"]: return self.CONCLUDED return self.INCONCLUSIVE if self.has_validator_set_changed(planet) else self.ONGOING - def has_validator_set_changed(self, planet): + def has_validator_set_changed(self, planet): # TODO: move somewhere else latest_change = self.get_validator_change(planet) if latest_change is None: return False @@ -251,13 +196,13 @@ class Election(Transaction): return latest_change_height > election["height"] - def get_election(self, election_id, planet): + def get_election(self, election_id, planet): # TODO: move somewhere else return planet.get_election(election_id) - def store(self, planet, height, is_concluded): + def store(self, planet, height, is_concluded): # TODO: move somewhere else planet.store_election(self.id, height, is_concluded) - def show_election(self, planet): + def show_election(self, planet): # TODO: move somewhere else data = self.asset["data"] if "public_key" in data.keys(): data["public_key"] = public_key_to_base64(data["public_key"]["value"]) @@ -270,7 +215,7 @@ class Election(Transaction): return response @classmethod - def _get_initiated_elections(cls, height, txns): + def _get_initiated_elections(cls, height, txns): # TODO: move somewhere else elections = [] for tx in txns: if not isinstance(tx, Election): @@ -280,7 +225,7 @@ class Election(Transaction): return elections @classmethod - def _get_votes(cls, txns): + def _get_votes(cls, txns): # TODO: move somewhere else elections = OrderedDict() for tx in txns: if not isinstance(tx, Vote): @@ -293,7 +238,7 @@ class Election(Transaction): return elections @classmethod - def process_block(cls, planet, new_height, txns): + def process_block(cls, planet, new_height, txns): # TODO: move somewhere else """Looks for election and vote transactions inside the block, records and processes elections. @@ -340,7 +285,7 @@ class Election(Transaction): return [validator_update] if validator_update else [] @classmethod - def rollback(cls, planet, new_height, txn_ids): + def rollback(cls, planet, new_height, txn_ids): # TODO: move somewhere else """Looks for election and vote transactions inside the block and cleans up the database artifacts possibly created in `process_blocks`. diff --git a/planetmint/transactions/types/elections/validator_election.py b/planetmint/transactions/types/elections/validator_election.py index 9aef155..814514d 100644 --- a/planetmint/transactions/types/elections/validator_election.py +++ b/planetmint/transactions/types/elections/validator_election.py @@ -19,25 +19,12 @@ class ValidatorElection(Election): ALLOWED_OPERATIONS = (OPERATION,) TX_SCHEMA_CUSTOM = TX_SCHEMA_VALIDATOR_ELECTION - def validate(self, planet, current_transactions=[]): - """For more details refer BEP-21: https://github.com/planetmint/BEPs/tree/master/21""" - - current_validators = self.get_validators(planet) - - super(ValidatorElection, self).validate(planet, current_transactions=current_transactions) - - # 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") - - return self - @classmethod def validate_schema(cls, tx): super(ValidatorElection, cls).validate_schema(tx) validate_asset_public_key(tx["asset"]["data"]["public_key"]) - def has_concluded(self, planet, *args, **kwargs): + def has_concluded(self, planet, *args, **kwargs): # TODO: move somewhere else latest_block = planet.get_latest_block() if latest_block is not None: latest_block_height = latest_block["height"] @@ -50,7 +37,7 @@ class ValidatorElection(Election): return super().has_concluded(planet, *args, **kwargs) - def on_approval(self, planet, new_height): + def on_approval(self, planet, new_height): # TODO: move somewhere else validator_updates = [self.asset["data"]] curr_validator_set = planet.get_validators(new_height) updated_validator_set = new_validator_set(curr_validator_set, validator_updates) @@ -61,6 +48,6 @@ class ValidatorElection(Election): planet.store_validator_set(new_height + 1, updated_validator_set) return encode_validator(self.asset["data"]) - def on_rollback(self, planetmint, new_height): + def on_rollback(self, planetmint, new_height): # TODO: move somewhere else # TODO change to `new_height + 2` when upgrading to Tendermint 0.24.0. planetmint.delete_validator_set(new_height + 1) diff --git a/tests/migrations/test_migration_election.py b/tests/migrations/test_migration_election.py index 7faf534..314daad 100644 --- a/tests/migrations/test_migration_election.py +++ b/tests/migrations/test_migration_election.py @@ -4,4 +4,4 @@ from planetmint.transactions.types.elections.chain_migration_election import Cha def test_valid_migration_election(b_mock, node_key): voters = ChainMigrationElection.recipients(b_mock) election = ChainMigrationElection.generate([node_key.public_key], voters, {}, None).sign([node_key.private_key]) - assert election.validate(b_mock) + assert b_mock.validate_election(election) diff --git a/tests/upsert_validator/test_validator_election.py b/tests/upsert_validator/test_validator_election.py index a43e38f..6a71fad 100644 --- a/tests/upsert_validator/test_validator_election.py +++ b/tests/upsert_validator/test_validator_election.py @@ -25,7 +25,7 @@ def test_upsert_validator_valid_election(b_mock, new_validator, node_key): election = ValidatorElection.generate([node_key.public_key], voters, new_validator, None).sign( [node_key.private_key] ) - assert election.validate(b_mock) + assert b_mock.validate_election(election) def test_upsert_validator_invalid_election_public_key(b_mock, new_validator, node_key): @@ -47,7 +47,7 @@ def test_upsert_validator_invalid_power_election(b_mock, new_validator, node_key [node_key.private_key] ) with pytest.raises(InvalidPowerChange): - election.validate(b_mock) + b_mock.validate_election(election) def test_upsert_validator_invalid_proposed_election(b_mock, new_validator, node_key): @@ -57,7 +57,7 @@ def test_upsert_validator_invalid_proposed_election(b_mock, new_validator, node_ 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) + b_mock.validate_election(election) def test_upsert_validator_invalid_inputs_election(b_mock, new_validator, node_key): @@ -69,7 +69,7 @@ def test_upsert_validator_invalid_inputs_election(b_mock, new_validator, node_ke [node_key.private_key, alice.private_key] ) with pytest.raises(MultipleInputsError): - election.validate(b_mock) + b_mock.validate_election(election) @patch("planetmint.transactions.types.elections.election.uuid4", lambda: "mock_uuid4") @@ -80,12 +80,12 @@ def test_upsert_validator_invalid_election(b_mock, new_validator, node_key, fixe ) with pytest.raises(DuplicateTransaction): - fixed_seed_election.validate(b_mock, [duplicate_election]) + b_mock.validate_election(fixed_seed_election, [duplicate_election]) b_mock.store_bulk_transactions([fixed_seed_election]) with pytest.raises(DuplicateTransaction): - duplicate_election.validate(b_mock) + b_mock.validate_election(duplicate_election) # Try creating an election with incomplete voter set invalid_election = ValidatorElection.generate([node_key.public_key], voters[1:], new_validator, None).sign( @@ -93,7 +93,7 @@ def test_upsert_validator_invalid_election(b_mock, new_validator, node_key, fixe ) with pytest.raises(UnequalValidatorSet): - invalid_election.validate(b_mock) + b_mock.validate_election(invalid_election) recipients = ValidatorElection.recipients(b_mock) altered_recipients = [] @@ -107,7 +107,7 @@ def test_upsert_validator_invalid_election(b_mock, new_validator, node_key, fixe ) with pytest.raises(UnequalValidatorSet): - tx_election.validate(b_mock) + b_mock.validate_election(tx_election) def test_get_status_ongoing(b, ongoing_validator_election, new_validator):