moved election validation to planetmint

Signed-off-by: Lorenz Herzberger <lorenzherzberger@gmail.com>
This commit is contained in:
Lorenz Herzberger 2022-09-28 20:55:53 +02:00
parent 9ba1423a16
commit e0d09198d9
No known key found for this signature in database
GPG Key ID: FA5EE906EB55316A
7 changed files with 125 additions and 105 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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