mirror of
https://github.com/bigchaindb/bigchaindb.git
synced 2024-10-13 13:34:05 +00:00
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:
parent
a0670b6d06
commit
7dcdefc58b
@ -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)
|
||||
|
@ -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')
|
||||
|
@ -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"""
|
||||
|
@ -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))
|
||||
|
@ -58,6 +58,7 @@ definitions:
|
||||
enum:
|
||||
- CREATE
|
||||
- TRANSFER
|
||||
- VALIDATOR_ELECTION
|
||||
asset:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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):
|
||||
|
2
bigchaindb/upsert_validator/__init__.py
Normal file
2
bigchaindb/upsert_validator/__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
|
||||
from bigchaindb.upsert_validator.validator_election import ValidatorElection # noqa
|
@ -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}$"
|
148
bigchaindb/upsert_validator/validator_election.py
Normal file
148
bigchaindb/upsert_validator/validator_election.py
Normal 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
|
@ -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__)
|
||||
|
||||
|
@ -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=='}
|
||||
|
0
tests/upsert_validator/__init__.py
Normal file
0
tests/upsert_validator/__init__.py
Normal file
32
tests/upsert_validator/conftest.py
Normal file
32
tests/upsert_validator/conftest.py
Normal 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
|
93
tests/upsert_validator/test_validator_election.py
Normal file
93
tests/upsert_validator/test_validator_election.py
Normal 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)
|
Loading…
x
Reference in New Issue
Block a user