mirror of
https://github.com/bigchaindb/bigchaindb.git
synced 2024-10-13 13:34:05 +00:00
Problem: (BEP-21) no way to propose validator election
Solution: Integrate the new election spec specifed in BEP-21
This commit is contained in:
parent
9cfc1e6f5a
commit
a8e5da31aa
@ -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')
|
||||
|
||||
@ -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))
|
||||
@ -939,7 +939,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 +986,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 +1011,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
|
||||
@ -1164,3 +1163,53 @@ class Transaction(object):
|
||||
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)
|
||||
|
||||
@ -101,56 +101,6 @@ class Transaction(Transaction):
|
||||
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.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
|
||||
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:
|
||||
"""A minimal wrapper around a transaction dictionary. This is useful for
|
||||
|
||||
@ -18,6 +18,7 @@ import requests
|
||||
import bigchaindb
|
||||
from bigchaindb import backend, config_utils
|
||||
from bigchaindb.models import Transaction
|
||||
from bigchaindb.validation_plugins import operation_class
|
||||
from bigchaindb.common.exceptions import (SchemaValidationError,
|
||||
ValidationError,
|
||||
DoubleSpend)
|
||||
@ -150,10 +151,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)
|
||||
@ -252,7 +253,7 @@ class BigchainDB(object):
|
||||
return backend.query.delete_unspent_outputs(
|
||||
self.connection, *unspent_outputs)
|
||||
|
||||
def get_transaction(self, transaction_id, include_status=False):
|
||||
def get_transaction(self, transaction_id, include_status=False, cls=Transaction):
|
||||
transaction = backend.query.get_transaction(self.connection, transaction_id)
|
||||
asset = backend.query.get_asset(self.connection, transaction_id)
|
||||
metadata = backend.query.get_metadata(self.connection, [transaction_id])
|
||||
@ -268,7 +269,7 @@ class BigchainDB(object):
|
||||
|
||||
transaction.update({'metadata': metadata})
|
||||
|
||||
transaction = Transaction.from_dict(transaction)
|
||||
transaction = cls.from_dict(transaction)
|
||||
|
||||
if include_status:
|
||||
return transaction, self.TX_VALID if transaction else None
|
||||
@ -326,7 +327,7 @@ class BigchainDB(object):
|
||||
if len(transactions) + len(current_spent_transactions) > 1:
|
||||
raise DoubleSpend('tx "{}" spends inputs twice'.format(txid))
|
||||
elif transactions:
|
||||
transaction = Transaction.from_db(self, transactions[0])
|
||||
transaction = operation_class(transactions[0]).from_db(self, transactions[0])
|
||||
elif current_spent_transactions:
|
||||
transaction = current_spent_transactions[0]
|
||||
|
||||
@ -364,7 +365,8 @@ class BigchainDB(object):
|
||||
|
||||
if block:
|
||||
transactions = backend.query.get_transactions(self.connection, block['transactions'])
|
||||
result['transactions'] = [t.to_dict() for t in Transaction.from_db(self, transactions)]
|
||||
cls = operation_class(transactions[0])
|
||||
result['transactions'] = [t.to_dict() for t in cls.from_db(self, transactions)]
|
||||
|
||||
return result
|
||||
|
||||
@ -392,9 +394,9 @@ 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)
|
||||
transaction = operation_class(tx).from_dict(tx)
|
||||
except SchemaValidationError as e:
|
||||
logger.warning('Invalid transaction schema: %s', e.__cause__.message)
|
||||
return False
|
||||
|
||||
@ -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}$"
|
||||
140
bigchaindb/upsert_validator/validator_election.py
Normal file
140
bigchaindb/upsert_validator/validator_election.py
Normal file
@ -0,0 +1,140 @@
|
||||
from bigchaindb.common.exceptions import (InvalidSignature,
|
||||
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, Input, Output
|
||||
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=None, outputs=None,
|
||||
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)
|
||||
|
||||
self.operation = self.VALIDATOR_ELECTION
|
||||
|
||||
@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.items() == voters.items())
|
||||
|
||||
def validate(self, bigchain, current_transactions=[]):
|
||||
"""Validate election transaction
|
||||
NOTE:
|
||||
* A valid election is initiated by an existing validator.
|
||||
|
||||
* A valid election is one where voters are validators and votes are
|
||||
alloacted according to the voting power of each validator node.
|
||||
|
||||
Args:
|
||||
bigchain (BigchainDB): an instantiated bigchaindb.tendermint.BigchainDB object.
|
||||
|
||||
Returns:
|
||||
The transaction (Transaction) if the transaction is valid else it
|
||||
raises an exception describing the reason why the transaction is
|
||||
invalid.
|
||||
|
||||
Raises:
|
||||
ValidationError: If the transaction is invalid
|
||||
"""
|
||||
input_conditions = []
|
||||
|
||||
duplicates = any(txn for txn in current_transactions if txn.id == self.id)
|
||||
if bigchain.get_transaction(self.to_dict()['id'], cls=ValidatorElection) 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:
|
||||
return False
|
||||
|
||||
# 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():
|
||||
return False
|
||||
|
||||
# NOTE: Check if all validators have been assigned votes equal to their voting power
|
||||
return self.is_same_topology(current_validators, self.outputs)
|
||||
|
||||
@classmethod
|
||||
def generate(cls, initiator, voters, election_data, metadata=None):
|
||||
election = cls.create(initiator, voters, metadata, asset=election_data)
|
||||
cls.validate_schema(election.to_dict())
|
||||
return election
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, tx):
|
||||
cls.validate_id(tx)
|
||||
|
||||
# The schema validation will ensure that the asset has been properly defined
|
||||
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(cls.VALIDATOR_ELECTION, tx['asset'], inputs, outputs, tx['metadata'],
|
||||
tx['version'], hash_id=tx['id'])
|
||||
|
||||
@classmethod
|
||||
def validate_schema(cls, tx):
|
||||
"""Validate the validator election transaction. Since `VALIDATOR_ELECTION` extends `CREATE`
|
||||
transaction, all the validations for `CREATE` transaction should be inherited
|
||||
"""
|
||||
_validate_schema(TX_SCHEMA_COMMON, tx)
|
||||
_validate_schema(TX_SCHEMA_CREATE, tx)
|
||||
_validate_schema(TX_SCHEMA_VALIDATOR_ELECTION, tx)
|
||||
15
bigchaindb/validation_plugins.py
Normal file
15
bigchaindb/validation_plugins.py
Normal file
@ -0,0 +1,15 @@
|
||||
from bigchaindb.models import Transaction
|
||||
from bigchaindb.upsert_validator import ValidatorElection
|
||||
|
||||
|
||||
OPERATION_TO_CLASS = {
|
||||
Transaction.CREATE: Transaction,
|
||||
Transaction.TRANSFER: Transaction,
|
||||
ValidatorElection.VALIDATOR_ELECTION: ValidatorElection
|
||||
}
|
||||
|
||||
|
||||
def operation_class(tx):
|
||||
"""For the given `tx` based on the `operation` key return its implementation class"""
|
||||
|
||||
return OPERATION_TO_CLASS.get(tx['operation'], Transaction)
|
||||
@ -8,9 +8,9 @@ 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.validation_plugins import operation_class
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -68,7 +68,7 @@ class TransactionListApi(Resource):
|
||||
tx = request.get_json(force=True)
|
||||
|
||||
try:
|
||||
tx_obj = Transaction.from_dict(tx)
|
||||
tx_obj = operation_class(tx).from_dict(tx)
|
||||
except SchemaValidationError as e:
|
||||
return make_error(
|
||||
400,
|
||||
|
||||
@ -18,6 +18,10 @@ from pymongo import MongoClient
|
||||
from bigchaindb.common import crypto
|
||||
from bigchaindb.log import setup_logging
|
||||
from bigchaindb.tendermint.lib import Block
|
||||
from bigchaindb.tendermint.utils import key_from_base64
|
||||
from bigchaindb.common.crypto import (key_pair_from_ed25519_key,
|
||||
public_key_from_ed25519_key)
|
||||
|
||||
|
||||
TEST_DB_NAME = 'bigchain_test'
|
||||
|
||||
@ -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
37
tests/upsert_validator/conftest.py
Normal file
37
tests/upsert_validator/conftest.py
Normal file
@ -0,0 +1,37 @@
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def b_mock(b, network_validators):
|
||||
b.write_transaction = mock_write_transaction
|
||||
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_write_transaction(tx):
|
||||
return (202, '')
|
||||
|
||||
|
||||
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
|
||||
48
tests/upsert_validator/test_validator_election.py
Normal file
48
tests/upsert_validator/test_validator_election.py
Normal file
@ -0,0 +1,48 @@
|
||||
import pytest
|
||||
|
||||
from bigchaindb.upsert_validator import ValidatorElection
|
||||
from bigchaindb.common.exceptions import DuplicateTransaction
|
||||
|
||||
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_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])
|
||||
|
||||
with pytest.raises(DuplicateTransaction):
|
||||
valid_election.validate(b_mock, [valid_election])
|
||||
|
||||
b_mock.store_bulk_transactions([valid_election])
|
||||
|
||||
with pytest.raises(DuplicateTransaction):
|
||||
valid_election.validate(b_mock, [valid_election])
|
||||
|
||||
# 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])
|
||||
assert not 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])
|
||||
|
||||
assert not tx_election.validate(b_mock)
|
||||
Loading…
x
Reference in New Issue
Block a user