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``
|
# the user wants to reconfigure the node. Check ``bigchaindb.config_utils``
|
||||||
# for more info.
|
# for more info.
|
||||||
_config = copy.deepcopy(config)
|
_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
|
PrivateKey = crypto.Ed25519SigningKey
|
||||||
PublicKey = crypto.Ed25519VerifyingKey
|
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):
|
class MultipleValidatorOperationError(ValidationError):
|
||||||
"""Raised when a validator update pending but new request is submited"""
|
"""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__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def _load_schema(name):
|
def _load_schema(name, path=__file__):
|
||||||
"""Load a schema from disk"""
|
"""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:
|
with open(path) as handle:
|
||||||
schema = yaml.safe_load(handle)
|
schema = yaml.safe_load(handle)
|
||||||
fast_schema = rapidjson_schema.loads(rapidjson.dumps(schema))
|
fast_schema = rapidjson_schema.loads(rapidjson.dumps(schema))
|
||||||
|
@ -58,6 +58,7 @@ definitions:
|
|||||||
enum:
|
enum:
|
||||||
- CREATE
|
- CREATE
|
||||||
- TRANSFER
|
- TRANSFER
|
||||||
|
- VALIDATOR_ELECTION
|
||||||
asset:
|
asset:
|
||||||
type: object
|
type: object
|
||||||
additionalProperties: false
|
additionalProperties: false
|
||||||
|
@ -515,7 +515,7 @@ class Transaction(object):
|
|||||||
version (string): Defines the version number of a Transaction.
|
version (string): Defines the version number of a Transaction.
|
||||||
hash_id (string): Hash id of the 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)
|
allowed_ops = ', '.join(self.__class__.ALLOWED_OPERATIONS)
|
||||||
raise ValueError('`operation` must be one of {}'
|
raise ValueError('`operation` must be one of {}'
|
||||||
.format(allowed_ops))
|
.format(allowed_ops))
|
||||||
@ -585,6 +585,38 @@ class Transaction(object):
|
|||||||
def _hash(self):
|
def _hash(self):
|
||||||
self._id = hash_data(self.serialized)
|
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
|
@classmethod
|
||||||
def create(cls, tx_signers, recipients, metadata=None, asset=None):
|
def create(cls, tx_signers, recipients, metadata=None, asset=None):
|
||||||
"""A simple way to generate a `CREATE` transaction.
|
"""A simple way to generate a `CREATE` transaction.
|
||||||
@ -613,32 +645,8 @@ class Transaction(object):
|
|||||||
Returns:
|
Returns:
|
||||||
:class:`~bigchaindb.common.transaction.Transaction`
|
: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)
|
return cls(cls.CREATE, {'data': asset}, inputs, outputs, metadata)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@ -939,7 +947,7 @@ class Transaction(object):
|
|||||||
Returns:
|
Returns:
|
||||||
bool: If all Inputs are valid.
|
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
|
# NOTE: Since in the case of a `CREATE`-transaction we do not have
|
||||||
# to check for outputs, we're just submitting dummy
|
# to check for outputs, we're just submitting dummy
|
||||||
# values to the actual method. This simplifies it's logic
|
# values to the actual method. This simplifies it's logic
|
||||||
@ -986,8 +994,7 @@ class Transaction(object):
|
|||||||
return all(validate(i, cond)
|
return all(validate(i, cond)
|
||||||
for i, cond in enumerate(output_condition_uris))
|
for i, cond in enumerate(output_condition_uris))
|
||||||
|
|
||||||
@staticmethod
|
def _input_valid(self, input_, operation, message, output_condition_uri=None):
|
||||||
def _input_valid(input_, operation, message, output_condition_uri=None):
|
|
||||||
"""Validates a single Input against a single Output.
|
"""Validates a single Input against a single Output.
|
||||||
|
|
||||||
Note:
|
Note:
|
||||||
@ -1012,7 +1019,7 @@ class Transaction(object):
|
|||||||
ParsingError, ASN1DecodeError, ASN1EncodeError):
|
ParsingError, ASN1DecodeError, ASN1EncodeError):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if operation == Transaction.CREATE:
|
if operation == self.CREATE:
|
||||||
# NOTE: In the case of a `CREATE` transaction, the
|
# NOTE: In the case of a `CREATE` transaction, the
|
||||||
# output is always valid.
|
# output is always valid.
|
||||||
output_valid = True
|
output_valid = True
|
||||||
@ -1151,7 +1158,7 @@ class Transaction(object):
|
|||||||
raise InvalidHash(err_msg.format(proposed_tx_id))
|
raise InvalidHash(err_msg.format(proposed_tx_id))
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_dict(cls, tx):
|
def from_dict(cls, tx, skip_schema_validation=True):
|
||||||
"""Transforms a Python dictionary to a Transaction object.
|
"""Transforms a Python dictionary to a Transaction object.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -1160,7 +1167,78 @@ class Transaction(object):
|
|||||||
Returns:
|
Returns:
|
||||||
:class:`~bigchaindb.common.transaction.Transaction`
|
: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']]
|
inputs = [Input.from_dict(input_) for input_ in tx['inputs']]
|
||||||
outputs = [Output.from_dict(output) for output in tx['outputs']]
|
outputs = [Output.from_dict(output) for output in tx['outputs']]
|
||||||
return cls(tx['operation'], tx['asset'], inputs, outputs,
|
return cls(tx['operation'], tx['asset'], inputs, outputs,
|
||||||
tx['metadata'], tx['version'], hash_id=tx['id'])
|
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 = []
|
txns = []
|
||||||
assets = []
|
assets = []
|
||||||
txn_metadatas = []
|
txn_metadatas = []
|
||||||
for transaction in transactions:
|
for transaction_obj in transactions:
|
||||||
# self.update_utxoset(transaction)
|
# self.update_utxoset(transaction)
|
||||||
transaction = transaction.to_dict()
|
transaction = transaction_obj.to_dict()
|
||||||
if transaction['operation'] == 'CREATE':
|
if transaction['operation'] == transaction_obj.CREATE:
|
||||||
asset = transaction.pop('asset')
|
asset = transaction.pop('asset')
|
||||||
asset['id'] = transaction['id']
|
asset['id'] = transaction['id']
|
||||||
assets.append(asset)
|
assets.append(asset)
|
||||||
@ -391,7 +391,7 @@ class BigchainDB(object):
|
|||||||
# CLEANUP: The conditional below checks for transaction in dict format.
|
# CLEANUP: The conditional below checks for transaction in dict format.
|
||||||
# It would be better to only have a single format for the transaction
|
# It would be better to only have a single format for the transaction
|
||||||
# throught the code base.
|
# throught the code base.
|
||||||
if not isinstance(transaction, Transaction):
|
if isinstance(transaction, dict):
|
||||||
try:
|
try:
|
||||||
transaction = Transaction.from_dict(tx)
|
transaction = Transaction.from_dict(tx)
|
||||||
except SchemaValidationError as e:
|
except SchemaValidationError as e:
|
||||||
|
@ -10,6 +10,7 @@ from bigchaindb.backend.schema import validate_language_key
|
|||||||
|
|
||||||
|
|
||||||
class Transaction(Transaction):
|
class Transaction(Transaction):
|
||||||
|
|
||||||
def validate(self, bigchain, current_transactions=[]):
|
def validate(self, bigchain, current_transactions=[]):
|
||||||
"""Validate transaction spend
|
"""Validate transaction spend
|
||||||
|
|
||||||
@ -94,62 +95,15 @@ class Transaction(Transaction):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_dict(cls, tx_body):
|
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_transaction_schema(tx_body)
|
||||||
validate_txn_obj('asset', tx_body['asset'], 'data', validate_key)
|
validate_txn_obj('asset', tx_body['asset'], 'data', validate_key)
|
||||||
validate_txn_obj('metadata', tx_body, 'metadata', validate_key)
|
validate_txn_obj('metadata', tx_body, 'metadata', validate_key)
|
||||||
validate_language_key(tx_body['asset'], 'data')
|
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:
|
class FastTransaction:
|
||||||
|
@ -75,12 +75,20 @@ def public_key64_to_address(base64_public_key):
|
|||||||
|
|
||||||
|
|
||||||
def public_key_from_base64(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):
|
def public_key_to_base64(ed25519_public_key):
|
||||||
ed25519_public_key = bytes.fromhex(ed25519_public_key)
|
return key_to_base64(ed25519_public_key)
|
||||||
return base64.b64encode(ed25519_public_key).decode('utf-8')
|
|
||||||
|
|
||||||
|
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):
|
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 flask_restful import Resource, reqparse
|
||||||
|
|
||||||
from bigchaindb.common.exceptions import SchemaValidationError, ValidationError
|
from bigchaindb.common.exceptions import SchemaValidationError, ValidationError
|
||||||
from bigchaindb.models import Transaction
|
|
||||||
from bigchaindb.web.views.base import make_error
|
from bigchaindb.web.views.base import make_error
|
||||||
from bigchaindb.web.views import parameters
|
from bigchaindb.web.views import parameters
|
||||||
|
from bigchaindb.models import Transaction
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -17,8 +17,12 @@ from pymongo import MongoClient
|
|||||||
|
|
||||||
from bigchaindb.common import crypto
|
from bigchaindb.common import crypto
|
||||||
from bigchaindb.log import setup_logging
|
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
|
from bigchaindb.lib import Block
|
||||||
|
|
||||||
|
|
||||||
TEST_DB_NAME = 'bigchain_test'
|
TEST_DB_NAME = 'bigchain_test'
|
||||||
|
|
||||||
USER2_SK, USER2_PK = crypto.generate_key_pair()
|
USER2_SK, USER2_PK = crypto.generate_key_pair()
|
||||||
@ -615,3 +619,41 @@ def utxoset(dummy_unspent_outputs, utxo_collection):
|
|||||||
assert res.acknowledged
|
assert res.acknowledged
|
||||||
assert len(res.inserted_ids) == 3
|
assert len(res.inserted_ids) == 3
|
||||||
return dummy_unspent_outputs, utxo_collection
|
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