Problem: There is no way to add new validators (BEP-21) (#2392).

Solution: Start by integrating the new election spec specifed in BEP-21.
This commit is contained in:
Vanshdeep Singh 2018-07-27 17:38:24 +02:00 committed by Lev Berman
parent a0670b6d06
commit 7dcdefc58b
17 changed files with 537 additions and 93 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -58,6 +58,7 @@ definitions:
enum:
- CREATE
- TRANSFER
- VALIDATOR_ELECTION
asset:
type: object
additionalProperties: false

View File

@ -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 `([<list of public keys>],'
' <amount>)`'))
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 `([<list of public keys>],'
' <amount>)`'))
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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,2 @@
from bigchaindb.upsert_validator.validator_election import ValidatorElection # noqa

View File

@ -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}$"

View File

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

View File

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

View File

@ -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=='}

View File

View File

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

View File

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