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) key = load_node_key(sk)
voters = election_class.recipients(planet) voters = election_class.recipients(planet)
election = election_class.generate([key.public_key], voters, data, None).sign([key.private_key]) 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: except ValidationError as e:
logger.error(e) logger.error(e)
return False return False

View File

@ -20,14 +20,29 @@ import requests
import planetmint import planetmint
from planetmint.config import Config from planetmint.config import Config
from planetmint import backend, config_utils, fastquery from planetmint import backend, config_utils, fastquery
from planetmint.transactions.common.transaction import Transaction from planetmint.transactions.common.transaction import VALIDATOR_ELECTION, Transaction
from planetmint.transactions.common.exceptions import DuplicateTransaction, InvalidSignature, SchemaValidationError, ValidationError, DoubleSpend, AmountError, InputDoesNotExist, AssetIdMismatch 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 ( from planetmint.transactions.common.transaction_mode_types import (
BROADCAST_TX_COMMIT, BROADCAST_TX_COMMIT,
BROADCAST_TX_ASYNC, BROADCAST_TX_ASYNC,
BROADCAST_TX_SYNC, 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 import exceptions as core_exceptions
from planetmint.validation import BaseValidationRules from planetmint.validation import BaseValidationRules
@ -600,6 +615,79 @@ class Planetmint(object):
tx = list(tx_map.values())[0] tx = list(tx_map.values())[0]
return Transaction.from_dict(tx) 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")) Block = namedtuple("Block", ("app_hash", "height", "transactions"))

View File

@ -12,7 +12,7 @@ class ChainMigrationElection(Election):
ALLOWED_OPERATIONS = (OPERATION,) ALLOWED_OPERATIONS = (OPERATION,)
TX_SCHEMA_CUSTOM = TX_SCHEMA_CHAIN_MIGRATION_ELECTION 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() chain = planetmint.get_latest_abci_chain()
if chain is not None and not chain["is_synced"]: if chain is not None and not chain["is_synced"]:
# do not conclude the migration election if # do not conclude the migration election if
@ -21,10 +21,10 @@ class ChainMigrationElection(Election):
return super().has_concluded(planetmint, *args, **kwargs) 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() planet.migrate_abci_chain()
def show_election(self, planet): def show_election(self, planet): # TODO: move somewhere else
output = super().show_election(planet) output = super().show_election(planet)
chain = planet.get_latest_abci_chain() chain = planet.get_latest_abci_chain()
if chain is None or chain["is_synced"]: if chain is None or chain["is_synced"]:
@ -46,5 +46,5 @@ class ChainMigrationElection(Election):
output += f"\nvalidators={json.dumps(validators, indent=4)}" output += f"\nvalidators={json.dumps(validators, indent=4)}"
return output 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) planet.delete_abci_chain(new_height)

View File

@ -9,8 +9,6 @@ from uuid import uuid4
from typing import Optional from typing import Optional
from planetmint import backend 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.types.elections.vote import Vote
from planetmint.transactions.common.exceptions import ( from planetmint.transactions.common.exceptions import (
InvalidSignature, InvalidSignature,
@ -44,7 +42,7 @@ class Election(Transaction):
ELECTION_THRESHOLD = 2 / 3 ELECTION_THRESHOLD = 2 / 3
@classmethod @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 the validator set from the most recent approved block
:return: { :return: {
@ -58,7 +56,7 @@ class Election(Transaction):
return planet.get_validator_change(latest_block["height"]) return planet.get_validator_change(latest_block["height"])
@classmethod @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 """Return a dictionary of validators with key as `public_key` and
value as the `voting_power` value as the `voting_power`
""" """
@ -71,7 +69,7 @@ class Election(Transaction):
return validators return validators
@classmethod @classmethod
def recipients(cls, planet): def recipients(cls, planet): # TODO: move somewhere else
"""Convert validator dictionary to a recipient list for `Transaction`""" """Convert validator dictionary to a recipient list for `Transaction`"""
recipients = [] recipients = []
@ -81,7 +79,7 @@ class Election(Transaction):
return recipients return recipients
@classmethod @classmethod
def is_same_topology(cls, current_topology, election_topology): def is_same_topology(cls, current_topology, election_topology): # TODO: move somewhere else
voters = {} voters = {}
for voter in election_topology: for voter in election_topology:
if len(voter.public_keys) > 1: if len(voter.public_keys) > 1:
@ -114,51 +112,6 @@ class Election(Transaction):
return True 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 @classmethod
def generate(cls, initiator, voters, election_data, metadata=None): def generate(cls, initiator, voters, election_data, metadata=None):
# Break symmetry in case we need to call an election with the same properties twice # 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) _validate_schema(cls.TX_SCHEMA_CUSTOM, tx)
@classmethod @classmethod
def create(cls, tx_signers, recipients, metadata=None, asset=None): def to_public_key(cls, election_id): # TODO: move somewhere else
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):
return base58.b58encode(bytes.fromhex(election_id)).decode() return base58.b58encode(bytes.fromhex(election_id)).decode()
@classmethod @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 votes = 0
for txn in transactions: for txn in transactions:
if getter(txn, "operation") == Vote.OPERATION: if getter(txn, "operation") == Vote.OPERATION:
@ -205,13 +150,13 @@ class Election(Transaction):
votes = votes + int(getter(output, "amount")) votes = votes + int(getter(output, "amount"))
return votes 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: if election_pk is None:
election_pk = self.to_public_key(self.id) election_pk = self.to_public_key(self.id)
txns = list(backend.query.get_asset_tokens_for_public_key(planet.connection, self.id, election_pk)) 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) 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. """Check if the election can be concluded or not.
* Elections can only be concluded if the validator set has not changed * Elections can only be concluded if the validator set has not changed
@ -233,14 +178,14 @@ class Election(Transaction):
return False return False
def get_status(self, planet): def get_status(self, planet): # TODO: move somewhere else
election = self.get_election(self.id, planet) election = self.get_election(self.id, planet)
if election and election["is_concluded"]: if election and election["is_concluded"]:
return self.CONCLUDED return self.CONCLUDED
return self.INCONCLUSIVE if self.has_validator_set_changed(planet) else self.ONGOING 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) latest_change = self.get_validator_change(planet)
if latest_change is None: if latest_change is None:
return False return False
@ -251,13 +196,13 @@ class Election(Transaction):
return latest_change_height > election["height"] 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) 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) 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"] data = self.asset["data"]
if "public_key" in data.keys(): if "public_key" in data.keys():
data["public_key"] = public_key_to_base64(data["public_key"]["value"]) data["public_key"] = public_key_to_base64(data["public_key"]["value"])
@ -270,7 +215,7 @@ class Election(Transaction):
return response return response
@classmethod @classmethod
def _get_initiated_elections(cls, height, txns): def _get_initiated_elections(cls, height, txns): # TODO: move somewhere else
elections = [] elections = []
for tx in txns: for tx in txns:
if not isinstance(tx, Election): if not isinstance(tx, Election):
@ -280,7 +225,7 @@ class Election(Transaction):
return elections return elections
@classmethod @classmethod
def _get_votes(cls, txns): def _get_votes(cls, txns): # TODO: move somewhere else
elections = OrderedDict() elections = OrderedDict()
for tx in txns: for tx in txns:
if not isinstance(tx, Vote): if not isinstance(tx, Vote):
@ -293,7 +238,7 @@ class Election(Transaction):
return elections return elections
@classmethod @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 """Looks for election and vote transactions inside the block, records
and processes elections. and processes elections.
@ -340,7 +285,7 @@ class Election(Transaction):
return [validator_update] if validator_update else [] return [validator_update] if validator_update else []
@classmethod @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 """Looks for election and vote transactions inside the block and
cleans up the database artifacts possibly created in `process_blocks`. cleans up the database artifacts possibly created in `process_blocks`.

View File

@ -19,25 +19,12 @@ class ValidatorElection(Election):
ALLOWED_OPERATIONS = (OPERATION,) ALLOWED_OPERATIONS = (OPERATION,)
TX_SCHEMA_CUSTOM = TX_SCHEMA_VALIDATOR_ELECTION 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 @classmethod
def validate_schema(cls, tx): def validate_schema(cls, tx):
super(ValidatorElection, cls).validate_schema(tx) super(ValidatorElection, cls).validate_schema(tx)
validate_asset_public_key(tx["asset"]["data"]["public_key"]) 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() latest_block = planet.get_latest_block()
if latest_block is not None: if latest_block is not None:
latest_block_height = latest_block["height"] latest_block_height = latest_block["height"]
@ -50,7 +37,7 @@ class ValidatorElection(Election):
return super().has_concluded(planet, *args, **kwargs) 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"]] validator_updates = [self.asset["data"]]
curr_validator_set = planet.get_validators(new_height) curr_validator_set = planet.get_validators(new_height)
updated_validator_set = new_validator_set(curr_validator_set, validator_updates) 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) planet.store_validator_set(new_height + 1, updated_validator_set)
return encode_validator(self.asset["data"]) 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. # TODO change to `new_height + 2` when upgrading to Tendermint 0.24.0.
planetmint.delete_validator_set(new_height + 1) 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): def test_valid_migration_election(b_mock, node_key):
voters = ChainMigrationElection.recipients(b_mock) voters = ChainMigrationElection.recipients(b_mock)
election = ChainMigrationElection.generate([node_key.public_key], voters, {}, None).sign([node_key.private_key]) 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( election = ValidatorElection.generate([node_key.public_key], voters, new_validator, None).sign(
[node_key.private_key] [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): 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] [node_key.private_key]
) )
with pytest.raises(InvalidPowerChange): 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): 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) voters = ValidatorElection.recipients(b_mock)
election = ValidatorElection.generate([alice.public_key], voters, new_validator, None).sign([alice.private_key]) election = ValidatorElection.generate([alice.public_key], voters, new_validator, None).sign([alice.private_key])
with pytest.raises(InvalidProposer): 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): 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] [node_key.private_key, alice.private_key]
) )
with pytest.raises(MultipleInputsError): with pytest.raises(MultipleInputsError):
election.validate(b_mock) b_mock.validate_election(election)
@patch("planetmint.transactions.types.elections.election.uuid4", lambda: "mock_uuid4") @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): 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]) b_mock.store_bulk_transactions([fixed_seed_election])
with pytest.raises(DuplicateTransaction): with pytest.raises(DuplicateTransaction):
duplicate_election.validate(b_mock) b_mock.validate_election(duplicate_election)
# Try creating an election with incomplete voter set # Try creating an election with incomplete voter set
invalid_election = ValidatorElection.generate([node_key.public_key], voters[1:], new_validator, None).sign( 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): with pytest.raises(UnequalValidatorSet):
invalid_election.validate(b_mock) b_mock.validate_election(invalid_election)
recipients = ValidatorElection.recipients(b_mock) recipients = ValidatorElection.recipients(b_mock)
altered_recipients = [] altered_recipients = []
@ -107,7 +107,7 @@ def test_upsert_validator_invalid_election(b_mock, new_validator, node_key, fixe
) )
with pytest.raises(UnequalValidatorSet): 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): def test_get_status_ongoing(b, ongoing_validator_election, new_validator):