mirror of
https://github.com/planetmint/planetmint.git
synced 2025-11-24 14:35:45 +00:00
moved election validation to planetmint
Signed-off-by: Lorenz Herzberger <lorenzherzberger@gmail.com>
This commit is contained in:
parent
9ba1423a16
commit
e0d09198d9
@ -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
|
||||
|
||||
@ -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"))
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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`.
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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):
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user