Create abstract election class (#2498).

* Problem: `ValidatorElection` and `MigrationElection` need to inherit from a common `Election` class.

Solution: Factored the common logic out of `ValidatorElection` and moved it to `Election` parent class.

* Problem: No need to store different types of elections in their own tables

Solution: Remove `DB_TABLE` property from `Election` class.

Solution: Created the `elections` table with secondary_index `election_id`.

* Problem: `UpsertValidatorVote` can be generalized to just be `Vote`

Solution: Renamed, refactored and moved the `Vote` class to tie in with the more general `Election` base class.

* Problem: `election_id` is not unique if two elections have the same properties.

Solution: Added a random `uuid4` seed to enforce uniqueness.
This commit is contained in:
Zachary Bowen 2018-09-11 15:39:46 +02:00 committed by Lev Berman
parent c79848d66a
commit 0fe749d830
24 changed files with 448 additions and 356 deletions

View File

@ -94,9 +94,9 @@ _config = copy.deepcopy(config)
from bigchaindb.common.transaction import Transaction # noqa from bigchaindb.common.transaction import Transaction # noqa
from bigchaindb import models # noqa from bigchaindb import models # noqa
from bigchaindb.upsert_validator import ValidatorElection # noqa from bigchaindb.upsert_validator import ValidatorElection # noqa
from bigchaindb.upsert_validator import ValidatorElectionVote # noqa from bigchaindb.elections.vote import Vote # noqa
Transaction.register_type(Transaction.CREATE, models.Transaction) Transaction.register_type(Transaction.CREATE, models.Transaction)
Transaction.register_type(Transaction.TRANSFER, models.Transaction) Transaction.register_type(Transaction.TRANSFER, models.Transaction)
Transaction.register_type(ValidatorElection.VALIDATOR_ELECTION, ValidatorElection) Transaction.register_type(ValidatorElection.OPERATION, ValidatorElection)
Transaction.register_type(ValidatorElectionVote.VALIDATOR_ELECTION_VOTE, ValidatorElectionVote) Transaction.register_type(Vote.OPERATION, Vote)

View File

@ -281,6 +281,18 @@ def store_validator_set(conn, validators_update):
) )
@register_query(LocalMongoDBConnection)
def store_election_results(conn, election):
height = election['height']
return conn.run(
conn.collection('elections').replace_one(
{'height': height},
election,
upsert=True
)
)
@register_query(LocalMongoDBConnection) @register_query(LocalMongoDBConnection)
def get_validator_set(conn, height=None): def get_validator_set(conn, height=None):
query = {} query = {}
@ -298,11 +310,11 @@ def get_validator_set(conn, height=None):
@register_query(LocalMongoDBConnection) @register_query(LocalMongoDBConnection)
def get_validator_set_by_election_id(conn, election_id): def get_election(conn, election_id):
query = {'election_id': election_id} query = {'election_id': election_id}
cursor = conn.run( cursor = conn.run(
conn.collection('validators') conn.collection('elections')
.find(query, projection={'_id': False}) .find(query, projection={'_id': False})
) )

View File

@ -48,6 +48,7 @@ def create_indexes(conn, dbname):
create_pre_commit_secondary_index(conn, dbname) create_pre_commit_secondary_index(conn, dbname)
create_validators_secondary_index(conn, dbname) create_validators_secondary_index(conn, dbname)
create_abci_chains_indexes(conn, dbname) create_abci_chains_indexes(conn, dbname)
create_elections_secondary_index(conn, dbname)
@register_schema(LocalMongoDBConnection) @register_schema(LocalMongoDBConnection)
@ -144,6 +145,15 @@ def create_abci_chains_indexes(conn, dbname):
unique=True,) unique=True,)
logger.info('Create `abci_chains.chain_id` secondary index.') logger.info('Create `abci_chains.chain_id` secondary index.')
conn.conn[dbname]['abci_chains'].create_index('chain_id', conn.conn[dbname]['abci_chains'].create_index('chain_id',
name='chain_id', name='chain_id',
unique=True) unique=True)
def create_elections_secondary_index(conn, dbname):
logger.info('Create `elections` secondary index.')
conn.conn[dbname]['elections'].create_index('election_id',
name='election_id',
unique=True,)

View File

@ -351,6 +351,13 @@ def store_validator_set(conn, validator_update):
raise NotImplementedError raise NotImplementedError
@singledispatch
def store_election_results(conn, election):
"""Store election results"""
raise NotImplementedError
@singledispatch @singledispatch
def get_validator_set(conn, height): def get_validator_set(conn, height):
"""Get validator set for a given `height`, if `height` is not specified """Get validator set for a given `height`, if `height` is not specified
@ -361,7 +368,7 @@ def get_validator_set(conn, height):
@singledispatch @singledispatch
def get_validator_set_by_election_id(conn, election_id): def get_election(conn, election_id):
"""Return a validator set change with the specified election_id """Return a validator set change with the specified election_id
""" """
@ -369,13 +376,11 @@ def get_validator_set_by_election_id(conn, election_id):
@singledispatch @singledispatch
def get_asset_tokens_for_public_key(connection, asset_id, def get_asset_tokens_for_public_key(connection, asset_id, public_key):
public_key, operation):
"""Retrieve a list of tokens of type `asset_id` that are owned by the `public_key`. """Retrieve a list of tokens of type `asset_id` that are owned by the `public_key`.
Args: Args:
asset_id (str): Id of the token. asset_id (str): Id of the token.
public_key (str): base58 encoded public key public_key (str): base58 encoded public key
operation: filter transaction based on `operation`
Returns: Returns:
Iterator of transaction that list given owner in conditions. Iterator of transaction that list given owner in conditions.
""" """

View File

@ -16,7 +16,7 @@ logger = logging.getLogger(__name__)
# Tables/collections that every backend database must create # Tables/collections that every backend database must create
TABLES = ('transactions', 'blocks', 'assets', 'metadata', TABLES = ('transactions', 'blocks', 'assets', 'metadata',
'validators', 'pre_commit', 'utxos', 'abci_chains') 'validators', 'elections', 'pre_commit', 'utxos', 'abci_chains')
VALID_LANGUAGES = ('danish', 'dutch', 'english', 'finnish', 'french', 'german', VALID_LANGUAGES = ('danish', 'dutch', 'english', 'finnish', 'french', 'german',
'hungarian', 'italian', 'norwegian', 'portuguese', 'romanian', 'hungarian', 'italian', 'norwegian', 'portuguese', 'romanian',

View File

@ -17,9 +17,10 @@ from bigchaindb.utils import load_node_key
from bigchaindb.common.exceptions import (DatabaseAlreadyExists, from bigchaindb.common.exceptions import (DatabaseAlreadyExists,
DatabaseDoesNotExist, DatabaseDoesNotExist,
ValidationError) ValidationError)
from bigchaindb.elections.vote import Vote
import bigchaindb import bigchaindb
from bigchaindb import (backend, ValidatorElection, from bigchaindb import (backend, ValidatorElection,
BigchainDB, ValidatorElectionVote) BigchainDB)
from bigchaindb.backend import schema from bigchaindb.backend import schema
from bigchaindb.backend import query from bigchaindb.backend import query
from bigchaindb.backend.query import PRE_COMMIT_ID from bigchaindb.backend.query import PRE_COMMIT_ID
@ -176,9 +177,9 @@ def run_upsert_validator_approve(args, bigchain):
inputs = [i for i in tx.to_inputs() if key.public_key in i.owners_before] inputs = [i for i in tx.to_inputs() if key.public_key in i.owners_before]
election_pub_key = ValidatorElection.to_public_key(tx.id) election_pub_key = ValidatorElection.to_public_key(tx.id)
approval = ValidatorElectionVote.generate(inputs, approval = Vote.generate(inputs,
[([election_pub_key], voting_power)], [([election_pub_key], voting_power)],
tx.id).sign([key.private_key]) tx.id).sign([key.private_key])
approval.validate(bigchain) approval.validate(bigchain)
resp = bigchain.write_transaction(approval, 'broadcast_tx_commit') resp = bigchain.write_transaction(approval, 'broadcast_tx_commit')

View File

@ -37,8 +37,7 @@ _, TX_SCHEMA_TRANSFER = _load_schema('transaction_transfer_' +
_, TX_SCHEMA_VALIDATOR_ELECTION = _load_schema('transaction_validator_election_' + _, TX_SCHEMA_VALIDATOR_ELECTION = _load_schema('transaction_validator_election_' +
TX_SCHEMA_VERSION) TX_SCHEMA_VERSION)
_, TX_SCHEMA_VALIDATOR_ELECTION_VOTE = _load_schema('transaction_validator_election_vote_' + _, TX_SCHEMA_VOTE = _load_schema('transaction_vote_' + TX_SCHEMA_VERSION)
TX_SCHEMA_VERSION)
def _validate_schema(schema, body): def _validate_schema(schema, body):

View File

@ -63,7 +63,7 @@ definitions:
- CREATE - CREATE
- TRANSFER - TRANSFER
- VALIDATOR_ELECTION - VALIDATOR_ELECTION
- VALIDATOR_ELECTION_VOTE - VOTE
asset: asset:
type: object type: object
additionalProperties: false additionalProperties: false

View File

@ -22,6 +22,8 @@ properties:
properties: properties:
node_id: node_id:
type: string type: string
seed:
type: string
public_key: public_key:
type: object type: object
additionalProperties: false additionalProperties: false

View File

@ -5,14 +5,14 @@
--- ---
"$schema": "http://json-schema.org/draft-04/schema#" "$schema": "http://json-schema.org/draft-04/schema#"
type: object type: object
title: Validator Election Vote Schema - Vote on a validator set change title: Vote Schema - Vote on an election
required: required:
- operation - operation
- outputs - outputs
properties: properties:
operation: operation:
type: string type: string
value: "VALIDATOR_ELECTION_VOTE" value: "VOTE"
outputs: outputs:
type: array type: array
items: items:

View File

@ -100,7 +100,7 @@ class App(BaseApplication):
block = Block(app_hash=app_hash, height=height, transactions=[]) block = Block(app_hash=app_hash, height=height, transactions=[])
self.bigchaindb.store_block(block._asdict()) self.bigchaindb.store_block(block._asdict())
self.bigchaindb.store_validator_set(height + 1, validator_set, None) self.bigchaindb.store_validator_set(height + 1, validator_set)
abci_chain_height = 0 if known_chain is None else known_chain['height'] abci_chain_height = 0 if known_chain is None else known_chain['height']
self.bigchaindb.store_abci_chain(abci_chain_height, self.bigchaindb.store_abci_chain(abci_chain_height,
genesis.chain_id, True) genesis.chain_id, True)
@ -209,9 +209,10 @@ class App(BaseApplication):
# Check if the current block concluded any validator elections and # Check if the current block concluded any validator elections and
# update the locally tracked validator set # update the locally tracked validator set
validator_updates = ValidatorElection.get_validator_update(self.bigchaindb, validator_update = ValidatorElection.approved_update(self.bigchaindb,
self.new_height, self.new_height,
self.block_transactions) self.block_transactions)
update = [validator_update] if validator_update else []
# Store pre-commit state to recover in case there is a crash # Store pre-commit state to recover in case there is a crash
# during `commit` # during `commit`
@ -220,7 +221,7 @@ class App(BaseApplication):
transactions=self.block_txn_ids) transactions=self.block_txn_ids)
logger.debug('Updating PreCommitState: %s', self.new_height) logger.debug('Updating PreCommitState: %s', self.new_height)
self.bigchaindb.store_pre_commit_state(pre_commit_state._asdict()) self.bigchaindb.store_pre_commit_state(pre_commit_state._asdict())
return ResponseEndBlock(validator_updates=validator_updates) return ResponseEndBlock(validator_updates=update)
def commit(self): def commit(self):
"""Store the new height and along with block hash.""" """Store the new height and along with block hash."""

View File

View File

@ -0,0 +1,254 @@
# Copyright BigchainDB GmbH and BigchainDB contributors
# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0)
# Code is Apache-2.0 and docs are CC-BY-4.0
import base58
from uuid import uuid4
from bigchaindb import backend
from bigchaindb.elections.vote import Vote
from bigchaindb.common.exceptions import (InvalidSignature,
MultipleInputsError,
InvalidProposer,
UnequalValidatorSet,
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 (_validate_schema,
TX_SCHEMA_COMMON,
TX_SCHEMA_CREATE)
class Election(Transaction):
# NOTE: this transaction class extends create so the operation inheritance is achieved
# by setting an ELECTION_TYPE and renaming CREATE = ELECTION_TYPE and ALLOWED_OPERATIONS = (ELECTION_TYPE,)
OPERATION = None
# Custom validation schema
TX_SCHEMA_CUSTOM = None
# Election Statuses:
ONGOING = 'ongoing'
CONCLUDED = 'concluded'
INCONCLUSIVE = 'inconclusive'
# Vote ratio to approve an election
ELECTION_THRESHOLD = 2 / 3
@classmethod
def get_validator_change(cls, bigchain, height=None):
"""Return the latest change to the validator set
:return: {
'height': <block_height>,
'validators': <validator_set>,
'election_id': <election_id_that_approved_the_change>
}
"""
return bigchain.get_validator_change(height)
@classmethod
def get_validators(cls, bigchain, height=None):
"""Return a dictionary of validators with key as `public_key` and
value as the `voting_power`
"""
validators = {}
for validator in bigchain.get_validators(height):
# NOTE: we assume that Tendermint encodes public key in base64
public_key = public_key_from_ed25519_key(key_from_base64(validator['public_key']['value']))
validators[public_key] = validator['voting_power']
return validators
@classmethod
def recipients(cls, bigchain):
"""Convert validator dictionary to a recipient list for `Transaction`"""
recipients = []
for public_key, voting_power in cls.get_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
NOTE:
* A valid election is initiated by an existing validator.
* A valid election is one where voters are validators and votes are
allocated according to the voting power of each validator node.
Args:
:param bigchain: (BigchainDB) an instantiated bigchaindb.lib.BigchainDB object.
:param current_transactions: (list) A list of transactions to be validated along with the election
Returns:
Election: a Election object or an object of the derived Election subclass.
Raises:
ValidationError: If the election is invalid
"""
input_conditions = []
duplicates = any(txn for txn in current_transactions if txn.id == self.id)
if bigchain.is_committed(self.id) or duplicates:
raise DuplicateTransaction('transaction `{}` already exists'
.format(self.id))
if not self.inputs_valid(input_conditions):
raise InvalidSignature('Transaction signature is invalid.')
current_validators = self.get_validators(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: Check if the proposer is a validator.
[election_initiator_node_pub_key] = self.inputs[0].owners_before
if election_initiator_node_pub_key not in current_validators.keys():
raise InvalidProposer('Public key is not a part of the validator set')
# NOTE: Check if all validators have been assigned votes equal to their voting power
if not self.is_same_topology(current_validators, self.outputs):
raise UnequalValidatorSet('Validator set much be exactly same to the outputs of election')
return self
@classmethod
def generate(cls, initiator, voters, election_data, metadata=None):
# Break symmetry in case we need to call an election with the same properties twice
uuid = uuid4()
election_data['seed'] = str(uuid)
(inputs, outputs) = cls.validate_create(initiator, voters, election_data, metadata)
election = cls(cls.OPERATION, {'data': election_data}, inputs, outputs, metadata)
cls.validate_schema(election.to_dict())
return election
@classmethod
def validate_schema(cls, tx):
"""Validate the election transaction. Since `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)
if cls.TX_SCHEMA_CUSTOM:
_validate_schema(cls.TX_SCHEMA_CUSTOM, 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
@classmethod
def to_public_key(cls, election_id):
return base58.b58encode(bytes.fromhex(election_id)).decode()
@classmethod
def count_votes(cls, election_pk, transactions, getter=getattr):
votes = 0
for txn in transactions:
if getter(txn, 'operation') == Vote.OPERATION:
for output in getter(txn, 'outputs'):
# NOTE: We enforce that a valid vote to election id will have only
# election_pk in the output public keys, including any other public key
# along with election_pk will lead to vote being not considered valid.
if len(getter(output, 'public_keys')) == 1 and [election_pk] == getter(output, 'public_keys'):
votes = votes + int(getter(output, 'amount'))
return votes
def get_commited_votes(self, bigchain, election_pk=None):
if election_pk is None:
election_pk = self.to_public_key(self.id)
txns = list(backend.query.get_asset_tokens_for_public_key(bigchain.connection,
self.id,
election_pk))
return self.count_votes(election_pk, txns, dict.get)
@classmethod
def has_concluded(cls, bigchain, election_id, current_votes=[], height=None):
"""Check if the given `election_id` can be concluded or not
NOTE:
* Election is concluded iff the current validator set is exactly equal
to the validator set encoded in election outputs
* Election can concluded only if the current votes achieves a supermajority
"""
election = bigchain.get_transaction(election_id)
if election:
election_pk = election.to_public_key(election.id)
votes_committed = election.get_commited_votes(bigchain, election_pk)
votes_current = election.count_votes(election_pk, current_votes)
current_validators = election.get_validators(bigchain, height)
if election.is_same_topology(current_validators, election.outputs):
total_votes = sum(current_validators.values())
if (votes_committed < (2/3)*total_votes) and \
(votes_committed + votes_current >= (2/3)*total_votes):
return election
return False
def get_status(self, bigchain):
concluded = self.get_election(self.id, bigchain)
if concluded:
return self.CONCLUDED
latest_change = self.get_validator_change(bigchain)
latest_change_height = latest_change['height']
election_height = bigchain.get_block_containing_tx(self.id)[0]
if latest_change_height >= election_height:
return self.INCONCLUSIVE
else:
return self.ONGOING
def get_election(self, election_id, bigchain):
result = bigchain.get_election(election_id)
return result
@classmethod
def store_election_results(cls, bigchain, election, height):
bigchain.store_election_results(height, election)
@classmethod
def approved_update(cls, bigchain, new_height, txns):
votes = {}
for txn in txns:
if not isinstance(txn, Vote):
continue
election_id = txn.asset['id']
election_votes = votes.get(election_id, [])
election_votes.append(txn)
votes[election_id] = election_votes
election = cls.has_concluded(bigchain, election_id, election_votes, new_height)
# Once an election concludes any other conclusion for the same
# or any other election is invalidated
if election:
cls.store_election_results(bigchain, election, new_height)
return cls.on_approval(bigchain, election, new_height)
return None
@classmethod
def on_approval(cls, bigchain, election, new_height):
raise NotImplementedError

View File

@ -6,16 +6,18 @@ from bigchaindb.common.transaction import Transaction
from bigchaindb.common.schema import (_validate_schema, from bigchaindb.common.schema import (_validate_schema,
TX_SCHEMA_COMMON, TX_SCHEMA_COMMON,
TX_SCHEMA_TRANSFER, TX_SCHEMA_TRANSFER,
TX_SCHEMA_VALIDATOR_ELECTION_VOTE) TX_SCHEMA_VOTE)
class ValidatorElectionVote(Transaction): class Vote(Transaction):
VALIDATOR_ELECTION_VOTE = 'VALIDATOR_ELECTION_VOTE' OPERATION = 'VOTE'
# NOTE: This class inherits TRANSFER txn type. The `TRANSFER` property is # NOTE: This class inherits TRANSFER txn type. The `TRANSFER` property is
# overriden to re-use methods from parent class # overriden to re-use methods from parent class
TRANSFER = VALIDATOR_ELECTION_VOTE TRANSFER = OPERATION
ALLOWED_OPERATIONS = (VALIDATOR_ELECTION_VOTE,) ALLOWED_OPERATIONS = (OPERATION,)
# Custom validation schema
TX_SCHEMA_CUSTOM = TX_SCHEMA_VOTE
def validate(self, bigchain, current_transactions=[]): def validate(self, bigchain, current_transactions=[]):
"""Validate election vote transaction """Validate election vote transaction
@ -28,7 +30,7 @@ class ValidatorElectionVote(Transaction):
bigchain (BigchainDB): an instantiated bigchaindb.lib.BigchainDB object. bigchain (BigchainDB): an instantiated bigchaindb.lib.BigchainDB object.
Returns: Returns:
ValidatorElectionVote object Vote: a Vote object
Raises: Raises:
ValidationError: If the election vote is invalid ValidationError: If the election vote is invalid
@ -39,20 +41,20 @@ class ValidatorElectionVote(Transaction):
@classmethod @classmethod
def generate(cls, inputs, recipients, election_id, metadata=None): def generate(cls, inputs, recipients, election_id, metadata=None):
(inputs, outputs) = cls.validate_transfer(inputs, recipients, election_id, metadata) (inputs, outputs) = cls.validate_transfer(inputs, recipients, election_id, metadata)
election_vote = cls(cls.VALIDATOR_ELECTION_VOTE, {'id': election_id}, inputs, outputs, metadata) election_vote = cls(cls.OPERATION, {'id': election_id}, inputs, outputs, metadata)
cls.validate_schema(election_vote.to_dict(), skip_id=True) cls.validate_schema(election_vote.to_dict(), skip_id=True)
return election_vote return election_vote
@classmethod @classmethod
def validate_schema(cls, tx, skip_id=False): def validate_schema(cls, tx, skip_id=False):
"""Validate the validator election vote transaction. Since `VALIDATOR_ELECTION_VOTE` extends `TRANFER` """Validate the validator election vote transaction. Since `VOTE` extends `TRANSFER`
transaction, all the validations for `CREATE` transaction should be inherited transaction, all the validations for `CREATE` transaction should be inherited
""" """
if not skip_id: if not skip_id:
cls.validate_id(tx) cls.validate_id(tx)
_validate_schema(TX_SCHEMA_COMMON, tx) _validate_schema(TX_SCHEMA_COMMON, tx)
_validate_schema(TX_SCHEMA_TRANSFER, tx) _validate_schema(TX_SCHEMA_TRANSFER, tx)
_validate_schema(TX_SCHEMA_VALIDATOR_ELECTION_VOTE, tx) _validate_schema(cls.TX_SCHEMA_CUSTOM, tx)
@classmethod @classmethod
def create(cls, tx_signers, recipients, metadata=None, asset=None): def create(cls, tx_signers, recipients, metadata=None, asset=None):

View File

@ -428,24 +428,20 @@ class BigchainDB(object):
result = self.get_validator_change(height) result = self.get_validator_change(height)
return [] if result is None else result['validators'] return [] if result is None else result['validators']
def get_validators_by_election_id(self, election_id): def get_election(self, election_id):
result = backend.query.get_validator_set_by_election_id(self.connection, election_id) result = backend.query.get_election(self.connection, election_id)
return result return result
def delete_validator_update(self):
return backend.query.delete_validator_update(self.connection)
def store_pre_commit_state(self, state): def store_pre_commit_state(self, state):
return backend.query.store_pre_commit_state(self.connection, state) return backend.query.store_pre_commit_state(self.connection, state)
def store_validator_set(self, height, validators, election_id): def store_validator_set(self, height, validators):
"""Store validator set at a given `height`. """Store validator set at a given `height`.
NOTE: If the validator set already exists at that `height` then an NOTE: If the validator set already exists at that `height` then an
exception will be raised. exception will be raised.
""" """
return backend.query.store_validator_set(self.connection, {'height': height, return backend.query.store_validator_set(self.connection, {'height': height,
'validators': validators, 'validators': validators})
'election_id': election_id})
def store_abci_chain(self, height, chain_id, is_synced=True): def store_abci_chain(self, height, chain_id, is_synced=True):
return backend.query.store_abci_chain(self.connection, height, return backend.query.store_abci_chain(self.connection, height,
@ -478,6 +474,14 @@ class BigchainDB(object):
self.store_abci_chain(block['height'] + 1, new_chain_id, False) self.store_abci_chain(block['height'] + 1, new_chain_id, False)
def store_election_results(self, height, election):
"""Store election results
:param height: the block height at which the election concluded
:param election: a concluded election
"""
return backend.query.store_election_results(self.connection, {'height': height,
'election_id': election.id})
Block = namedtuple('Block', ('app_hash', 'height', 'transactions')) Block = namedtuple('Block', ('app_hash', 'height', 'transactions'))

View File

@ -3,5 +3,4 @@
# Code is Apache-2.0 and docs are CC-BY-4.0 # Code is Apache-2.0 and docs are CC-BY-4.0
from bigchaindb.upsert_validator.validator_election_vote import ValidatorElectionVote # noqa
from bigchaindb.upsert_validator.validator_election import ValidatorElection # noqa from bigchaindb.upsert_validator.validator_election import ValidatorElection # noqa

View File

@ -2,264 +2,48 @@
# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) # SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0)
# Code is Apache-2.0 and docs are CC-BY-4.0 # Code is Apache-2.0 and docs are CC-BY-4.0
import base58 from bigchaindb.common.exceptions import InvalidPowerChange
from bigchaindb.elections.election import Election
from bigchaindb import backend from bigchaindb.common.schema import (TX_SCHEMA_VALIDATOR_ELECTION)
from bigchaindb.common.exceptions import (InvalidSignature, from .validator_utils import (new_validator_set, encode_validator, validate_asset_public_key)
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 (_validate_schema,
TX_SCHEMA_VALIDATOR_ELECTION,
TX_SCHEMA_COMMON,
TX_SCHEMA_CREATE)
from . import ValidatorElectionVote
from .validator_utils import (new_validator_set,
encode_validator,
encode_pk_to_base16,
validate_asset_public_key)
class ValidatorElection(Transaction): class ValidatorElection(Election):
VALIDATOR_ELECTION = 'VALIDATOR_ELECTION' OPERATION = 'VALIDATOR_ELECTION'
# NOTE: this transaction class extends create so the operation inheritence is achieved # NOTE: this transaction class extends create so the operation inheritence is achieved
# by renaming CREATE to VALIDATOR_ELECTION # by renaming CREATE to VALIDATOR_ELECTION
CREATE = VALIDATOR_ELECTION CREATE = OPERATION
ALLOWED_OPERATIONS = (VALIDATOR_ELECTION,) ALLOWED_OPERATIONS = (OPERATION,)
# Election Statuses: TX_SCHEMA_CUSTOM = TX_SCHEMA_VALIDATOR_ELECTION
ONGOING = 'ongoing'
CONCLUDED = 'concluded'
INCONCLUSIVE = 'inconclusive'
ELECTION_THRESHOLD = 2 / 3
@classmethod
def get_validator_change(cls, bigchain, height=None):
"""Return the latest change to the validator set
:return: {
'height': <block_height>,
'asset': {
'height': <block_height>,
'validators': <validator_set>,
'election_id': <election_id_that_approved_the_change>
}
}
"""
return bigchain.get_validator_change(height)
@classmethod
def get_validators(cls, bigchain, height=None):
"""Return a dictionary of validators with key as `public_key` and
value as the `voting_power`
"""
validators = {}
for validator in bigchain.get_validators(height):
# NOTE: we assume that Tendermint encodes public key in base64
public_key = public_key_from_ed25519_key(key_from_base64(validator['public_key']['value']))
validators[public_key] = validator['voting_power']
return validators
@classmethod
def recipients(cls, bigchain):
"""Convert validator dictionary to a recipient list for `Transaction`"""
recipients = []
for public_key, voting_power in cls.get_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=[]): def validate(self, bigchain, current_transactions=[]):
"""Validate election transaction """For more details refer BEP-21: https://github.com/bigchaindb/BEPs/tree/master/21
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:
ValidatorElection object
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.get_validators(bigchain) current_validators = self.get_validators(bigchain)
# NOTE: Proposer should be a single node super(ValidatorElection, self).validate(bigchain, current_transactions=current_transactions)
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 # NOTE: change more than 1/3 of the current power is not allowed
if self.asset['data']['power'] >= (1/3)*sum(current_validators.values()): if self.asset['data']['power'] >= (1/3)*sum(current_validators.values()):
raise InvalidPowerChange('`power` change must be less than 1/3 of total power') 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 self return self
@classmethod @classmethod
def generate(cls, initiator, voters, election_data, metadata=None): def validate_schema(cls, tx):
(inputs, outputs) = cls.validate_create(initiator, voters, election_data, metadata) super(ValidatorElection, cls).validate_schema(tx)
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)
validate_asset_public_key(tx['asset']['data']['public_key']) validate_asset_public_key(tx['asset']['data']['public_key'])
@classmethod @classmethod
def create(cls, tx_signers, recipients, metadata=None, asset=None): def on_approval(cls, bigchain, election, new_height):
raise NotImplementedError # The new validator set comes into effect from height = new_height+1
validator_updates = [election.asset['data']]
curr_validator_set = bigchain.get_validators(new_height)
updated_validator_set = new_validator_set(curr_validator_set,
validator_updates)
@classmethod updated_validator_set = [v for v in updated_validator_set if v['voting_power'] > 0]
def transfer(cls, tx_signers, recipients, metadata=None, asset=None): bigchain.store_validator_set(new_height+1, updated_validator_set)
raise NotImplementedError return encode_validator(election.asset['data'])
@classmethod
def to_public_key(cls, election_id):
return base58.b58encode(bytes.fromhex(election_id)).decode()
@classmethod
def count_votes(cls, election_pk, transactions, getter=getattr):
votes = 0
for txn in transactions:
if getter(txn, 'operation') == 'VALIDATOR_ELECTION_VOTE':
for output in getter(txn, 'outputs'):
# NOTE: We enforce that a valid vote to election id will have only
# election_pk in the output public keys, including any other public key
# along with election_pk will lead to vote being not considered valid.
if len(getter(output, 'public_keys')) == 1 and [election_pk] == getter(output, 'public_keys'):
votes = votes + int(getter(output, 'amount'))
return votes
def get_commited_votes(self, bigchain, election_pk=None):
if election_pk is None:
election_pk = self.to_public_key(self.id)
txns = list(backend.query.get_asset_tokens_for_public_key(bigchain.connection,
self.id,
election_pk))
return self.count_votes(election_pk, txns, dict.get)
@classmethod
def has_concluded(cls, bigchain, election_id, current_votes=[], height=None):
"""Check if the given `election_id` can be concluded or not
NOTE:
* Election is concluded iff the current validator set is exactly equal
to the validator set encoded in election outputs
* Election can concluded only if the current votes achieves a supermajority
"""
election = bigchain.get_transaction(election_id)
if election:
election_pk = election.to_public_key(election.id)
votes_commited = election.get_commited_votes(bigchain, election_pk)
votes_current = election.count_votes(election_pk, current_votes)
current_validators = election.get_validators(bigchain, height)
if election.is_same_topology(current_validators, election.outputs):
total_votes = sum(current_validators.values())
if (votes_commited < (2/3)*total_votes) and \
(votes_commited + votes_current >= (2/3)*total_votes):
return election
return False
@classmethod
def get_validator_update(cls, bigchain, new_height, txns):
votes = {}
for txn in txns:
if not isinstance(txn, ValidatorElectionVote):
continue
election_id = txn.asset['id']
election_votes = votes.get(election_id, [])
election_votes.append(txn)
votes[election_id] = election_votes
election = cls.has_concluded(bigchain, election_id, election_votes, new_height)
# Once an election concludes any other conclusion for the same
# or any other election is invalidated
if election:
# The new validator set comes into effect from height = new_height+1
validator_updates = [election.asset['data']]
curr_validator_set = bigchain.get_validators(new_height)
updated_validator_set = new_validator_set(curr_validator_set,
validator_updates)
updated_validator_set = [v for v in updated_validator_set if v['voting_power'] > 0]
bigchain.store_validator_set(new_height+1, updated_validator_set, election.id)
validator16 = encode_pk_to_base16(election.asset['data'])
return [encode_validator(validator16)]
return []
def get_validator_update_by_election_id(self, election_id, bigchain):
result = bigchain.get_validators_by_election_id(election_id)
return result
def get_status(self, bigchain):
concluded = self.get_validator_update_by_election_id(self.id, bigchain)
if concluded:
return self.CONCLUDED
latest_change = self.get_validator_change(bigchain)
latest_change_height = latest_change['height']
election_height = bigchain.get_block_containing_tx(self.id)[0]
if latest_change_height >= election_height:
return self.INCONCLUSIVE
else:
return self.ONGOING

View File

@ -21,7 +21,7 @@ def test_init_creates_db_tables_and_indexes():
collection_names = conn.conn[dbname].collection_names() collection_names = conn.conn[dbname].collection_names()
assert set(collection_names) == { assert set(collection_names) == {
'transactions', 'assets', 'metadata', 'blocks', 'utxos', 'pre_commit', 'transactions', 'assets', 'metadata', 'blocks', 'utxos', 'pre_commit',
'validators', 'abci_chains', 'validators', 'elections', 'abci_chains',
} }
indexes = conn.conn[dbname]['assets'].index_information().keys() indexes = conn.conn[dbname]['assets'].index_information().keys()
@ -46,6 +46,9 @@ def test_init_creates_db_tables_and_indexes():
indexes = conn.conn[dbname]['abci_chains'].index_information().keys() indexes = conn.conn[dbname]['abci_chains'].index_information().keys()
assert set(indexes) == {'_id_', 'height', 'chain_id'} assert set(indexes) == {'_id_', 'height', 'chain_id'}
indexes = conn.conn[dbname]['elections'].index_information().keys()
assert set(indexes) == {'_id_', 'election_id'}
def test_init_database_fails_if_db_exists(): def test_init_database_fails_if_db_exists():
import bigchaindb import bigchaindb
@ -78,7 +81,7 @@ def test_create_tables():
collection_names = conn.conn[dbname].collection_names() collection_names = conn.conn[dbname].collection_names()
assert set(collection_names) == { assert set(collection_names) == {
'transactions', 'assets', 'metadata', 'blocks', 'utxos', 'validators', 'transactions', 'assets', 'metadata', 'blocks', 'utxos', 'validators', 'elections',
'pre_commit', 'abci_chains', 'pre_commit', 'abci_chains',
} }
@ -117,6 +120,10 @@ def test_create_secondary_indexes():
assert index_info['utxo']['key'] == [('transaction_id', 1), assert index_info['utxo']['key'] == [('transaction_id', 1),
('output_index', 1)] ('output_index', 1)]
indexes = conn.conn[dbname]['elections'].index_information()
assert set(indexes.keys()) == {'_id_', 'election_id'}
assert indexes['election_id']['unique']
indexes = conn.conn[dbname]['pre_commit'].index_information() indexes = conn.conn[dbname]['pre_commit'].index_information()
assert set(indexes.keys()) == {'_id_', 'pre_commit_id'} assert set(indexes.keys()) == {'_id_', 'pre_commit_id'}
assert indexes['pre_commit_id']['unique'] assert indexes['pre_commit_id']['unique']

View File

@ -366,8 +366,8 @@ def test_end_block_return_validator_updates(b, init_chain_request):
resp = app.end_block(RequestEndBlock(height=99)) resp = app.end_block(RequestEndBlock(height=99))
assert resp.validator_updates[0] == encode_validator(validator) assert resp.validator_updates[0] == encode_validator(validator)
updates = b.get_validator_update() updates = b.approved_update()
assert updates == [] assert not updates
def test_store_pre_commit_state_in_end_block(b, alice, init_chain_request): def test_store_pre_commit_state_in_end_block(b, alice, init_chain_request):
@ -427,7 +427,7 @@ def test_new_validator_set(b):
validators = [node1] validators = [node1]
updates = [node1_new_power, node2] updates = [node1_new_power, node2]
b.store_validator_set(1, validators, 'election_id') b.store_validator_set(1, validators)
updated_validator_set = new_validator_set(b.get_validators(1), updates) updated_validator_set = new_validator_set(b.get_validators(1), updates)
updated_validators = [] updated_validators = []

View File

@ -161,11 +161,11 @@ def test_validator_updates(b, validator_pub_key):
'update_id': VALIDATOR_UPDATE_ID} 'update_id': VALIDATOR_UPDATE_ID}
query.store_validator_update(b.connection, validator_update) query.store_validator_update(b.connection, validator_update)
updates = b.get_validator_update() updates = b.approved_updates()
assert updates == [validator_update['validator']] assert updates == validator_update['validator']
b.delete_validator_update() b.delete_validator_update()
assert b.get_validator_update() == [] assert not b.approved_updates()
@pytest.mark.bdb @pytest.mark.bdb

View File

@ -1,10 +1,11 @@
# Copyright BigchainDB GmbH and BigchainDB contributors # Copyright BigchainDB GmbH and BigchainDB contributors
# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) # SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0)
# Code is Apache-2.0 and docs are CC-BY-4.0 # Code is Apache-2.0 and docs are CC-BY-4.0
from unittest.mock import patch
import pytest import pytest
from bigchaindb import ValidatorElectionVote from bigchaindb import Vote
from bigchaindb.backend.localmongodb import query from bigchaindb.backend.localmongodb import query
from bigchaindb.lib import Block from bigchaindb.lib import Block
from bigchaindb.upsert_validator import ValidatorElection from bigchaindb.upsert_validator import ValidatorElection
@ -46,6 +47,15 @@ def valid_election_b(b, node_key, new_validator):
new_validator, None).sign([node_key.private_key]) new_validator, None).sign([node_key.private_key])
@pytest.fixture
@patch('bigchaindb.elections.election.uuid4', lambda: 'mock_uuid4')
def fixed_seed_election(b_mock, node_key, new_validator):
voters = ValidatorElection.recipients(b_mock)
return ValidatorElection.generate([node_key.public_key],
voters,
new_validator, None).sign([node_key.private_key])
@pytest.fixture @pytest.fixture
def ongoing_election(b, valid_election, ed25519_node_keys): def ongoing_election(b, valid_election, ed25519_node_keys):
validators = b.get_validators(height=1) validators = b.get_validators(height=1)
@ -62,12 +72,10 @@ def ongoing_election(b, valid_election, ed25519_node_keys):
@pytest.fixture @pytest.fixture
def concluded_election(b, ongoing_election, ed25519_node_keys): def concluded_election(b, ongoing_election, ed25519_node_keys):
validators = b.get_validators(height=1) election_result = {'height': 2,
validator_update = {'validators': validators, 'election_id': ongoing_election.id}
'height': 2,
'election_id': ongoing_election.id}
query.store_validator_set(b.connection, validator_update) query.store_election_results(b.connection, election_result)
return ongoing_election return ongoing_election
@ -91,9 +99,9 @@ def vote(election, voter, keys, b):
election_pub_key = ValidatorElection.to_public_key(election.id) election_pub_key = ValidatorElection.to_public_key(election.id)
v = ValidatorElectionVote.generate([election_input], v = Vote.generate([election_input],
[([election_pub_key], votes)], [([election_pub_key], votes)],
election_id=election.id)\ election_id=election.id)\
.sign([key.private_key]) .sign([key.private_key])
b.store_bulk_transactions([v]) b.store_bulk_transactions([v])
return v return v

View File

@ -6,10 +6,11 @@ import pytest
import codecs import codecs
from bigchaindb.tendermint_utils import public_key_to_base64 from bigchaindb.tendermint_utils import public_key_to_base64
from bigchaindb.upsert_validator import ValidatorElection, ValidatorElectionVote from bigchaindb.upsert_validator import ValidatorElection
from bigchaindb.common.exceptions import AmountError from bigchaindb.common.exceptions import AmountError
from bigchaindb.common.crypto import generate_key_pair from bigchaindb.common.crypto import generate_key_pair
from bigchaindb.common.exceptions import ValidationError from bigchaindb.common.exceptions import ValidationError
from bigchaindb.elections.vote import Vote
from tests.utils import generate_block from tests.utils import generate_block
pytestmark = [pytest.mark.execute] pytestmark = [pytest.mark.execute]
@ -26,10 +27,10 @@ def test_upsert_validator_valid_election_vote(b_mock, valid_election, ed25519_no
election_pub_key = ValidatorElection.to_public_key(valid_election.id) election_pub_key = ValidatorElection.to_public_key(valid_election.id)
vote = ValidatorElectionVote.generate([input0], vote = Vote.generate([input0],
[([election_pub_key], votes)], [([election_pub_key], votes)],
election_id=valid_election.id)\ election_id=valid_election.id)\
.sign([key0.private_key]) .sign([key0.private_key])
assert vote.validate(b_mock) assert vote.validate(b_mock)
@ -46,10 +47,10 @@ def test_upsert_validator_valid_non_election_vote(b_mock, valid_election, ed2551
# Ensure that threshold conditions are now allowed # Ensure that threshold conditions are now allowed
with pytest.raises(ValidationError): with pytest.raises(ValidationError):
ValidatorElectionVote.generate([input0], Vote.generate([input0],
[([election_pub_key, key0.public_key], votes)], [([election_pub_key, key0.public_key], votes)],
election_id=valid_election.id)\ election_id=valid_election.id)\
.sign([key0.private_key]) .sign([key0.private_key])
@pytest.mark.bdb @pytest.mark.bdb
@ -63,10 +64,10 @@ def test_upsert_validator_delegate_election_vote(b_mock, valid_election, ed25519
public_key0 = input0.owners_before[0] public_key0 = input0.owners_before[0]
key0 = ed25519_node_keys[public_key0] key0 = ed25519_node_keys[public_key0]
delegate_vote = ValidatorElectionVote.generate([input0], delegate_vote = Vote.generate([input0],
[([alice.public_key], 3), ([key0.public_key], votes-3)], [([alice.public_key], 3), ([key0.public_key], votes-3)],
election_id=valid_election.id)\ election_id=valid_election.id)\
.sign([key0.private_key]) .sign([key0.private_key])
assert delegate_vote.validate(b_mock) assert delegate_vote.validate(b_mock)
@ -74,17 +75,17 @@ def test_upsert_validator_delegate_election_vote(b_mock, valid_election, ed25519
election_pub_key = ValidatorElection.to_public_key(valid_election.id) election_pub_key = ValidatorElection.to_public_key(valid_election.id)
alice_votes = delegate_vote.to_inputs()[0] alice_votes = delegate_vote.to_inputs()[0]
alice_casted_vote = ValidatorElectionVote.generate([alice_votes], alice_casted_vote = Vote.generate([alice_votes],
[([election_pub_key], 3)], [([election_pub_key], 3)],
election_id=valid_election.id)\ election_id=valid_election.id)\
.sign([alice.private_key]) .sign([alice.private_key])
assert alice_casted_vote.validate(b_mock) assert alice_casted_vote.validate(b_mock)
key0_votes = delegate_vote.to_inputs()[1] key0_votes = delegate_vote.to_inputs()[1]
key0_casted_vote = ValidatorElectionVote.generate([key0_votes], key0_casted_vote = Vote.generate([key0_votes],
[([election_pub_key], votes-3)], [([election_pub_key], votes-3)],
election_id=valid_election.id)\ election_id=valid_election.id)\
.sign([key0.private_key]) .sign([key0.private_key])
assert key0_casted_vote.validate(b_mock) assert key0_casted_vote.validate(b_mock)
@ -99,10 +100,10 @@ def test_upsert_validator_invalid_election_vote(b_mock, valid_election, ed25519_
election_pub_key = ValidatorElection.to_public_key(valid_election.id) election_pub_key = ValidatorElection.to_public_key(valid_election.id)
vote = ValidatorElectionVote.generate([input0], vote = Vote.generate([input0],
[([election_pub_key], votes+1)], [([election_pub_key], votes+1)],
election_id=valid_election.id)\ election_id=valid_election.id)\
.sign([key0.private_key]) .sign([key0.private_key])
with pytest.raises(AmountError): with pytest.raises(AmountError):
assert vote.validate(b_mock) assert vote.validate(b_mock)
@ -120,10 +121,10 @@ def test_valid_election_votes_received(b_mock, valid_election, ed25519_node_keys
key0 = ed25519_node_keys[public_key0] key0 = ed25519_node_keys[public_key0]
# delegate some votes to alice # delegate some votes to alice
delegate_vote = ValidatorElectionVote.generate([input0], delegate_vote = Vote.generate([input0],
[([alice.public_key], 4), ([key0.public_key], votes-4)], [([alice.public_key], 4), ([key0.public_key], votes-4)],
election_id=valid_election.id)\ election_id=valid_election.id)\
.sign([key0.private_key]) .sign([key0.private_key])
b_mock.store_bulk_transactions([delegate_vote]) b_mock.store_bulk_transactions([delegate_vote])
assert valid_election.get_commited_votes(b_mock) == 0 assert valid_election.get_commited_votes(b_mock) == 0
@ -131,10 +132,10 @@ def test_valid_election_votes_received(b_mock, valid_election, ed25519_node_keys
alice_votes = delegate_vote.to_inputs()[0] alice_votes = delegate_vote.to_inputs()[0]
key0_votes = delegate_vote.to_inputs()[1] key0_votes = delegate_vote.to_inputs()[1]
alice_casted_vote = ValidatorElectionVote.generate([alice_votes], alice_casted_vote = Vote.generate([alice_votes],
[([election_public_key], 2), ([alice.public_key], 2)], [([election_public_key], 2), ([alice.public_key], 2)],
election_id=valid_election.id)\ election_id=valid_election.id)\
.sign([alice.private_key]) .sign([alice.private_key])
assert alice_casted_vote.validate(b_mock) assert alice_casted_vote.validate(b_mock)
b_mock.store_bulk_transactions([alice_casted_vote]) b_mock.store_bulk_transactions([alice_casted_vote])
@ -142,10 +143,10 @@ def test_valid_election_votes_received(b_mock, valid_election, ed25519_node_keys
# Check if the delegated vote is count as valid vote # Check if the delegated vote is count as valid vote
assert valid_election.get_commited_votes(b_mock) == 2 assert valid_election.get_commited_votes(b_mock) == 2
key0_casted_vote = ValidatorElectionVote.generate([key0_votes], key0_casted_vote = Vote.generate([key0_votes],
[([election_public_key], votes-4)], [([election_public_key], votes-4)],
election_id=valid_election.id)\ election_id=valid_election.id)\
.sign([key0.private_key]) .sign([key0.private_key])
assert key0_casted_vote.validate(b_mock) assert key0_casted_vote.validate(b_mock)
b_mock.store_bulk_transactions([key0_casted_vote]) b_mock.store_bulk_transactions([key0_casted_vote])
@ -227,7 +228,7 @@ def test_upsert_validator(b, node_key, node_keys, ed25519_node_keys):
latest_block = b.get_latest_block() latest_block = b.get_latest_block()
# reset the validator set # reset the validator set
b.store_validator_set(latest_block['height'], validators, 'previous_election_id') b.store_validator_set(latest_block['height'], validators)
power = 1 power = 1
public_key = '9B3119650DF82B9A5D8A12E38953EA47475C09F0C48A4E6A0ECE182944B24403' public_key = '9B3119650DF82B9A5D8A12E38953EA47475C09F0C48A4E6A0ECE182944B24403'
@ -291,19 +292,19 @@ def test_get_validator_update(b, node_keys, node_key, ed25519_node_keys):
assert not ValidatorElection.has_concluded(b, election.id, [tx_vote0, tx_vote1]) assert not ValidatorElection.has_concluded(b, election.id, [tx_vote0, tx_vote1])
assert ValidatorElection.has_concluded(b, election.id, [tx_vote0, tx_vote1, tx_vote2]) assert ValidatorElection.has_concluded(b, election.id, [tx_vote0, tx_vote1, tx_vote2])
assert ValidatorElection.get_validator_update(b, 4, [tx_vote0]) == [] assert not ValidatorElection.approved_update(b, 4, [tx_vote0])
assert ValidatorElection.get_validator_update(b, 4, [tx_vote0, tx_vote1]) == [] assert not ValidatorElection.approved_update(b, 4, [tx_vote0, tx_vote1])
update = ValidatorElection.get_validator_update(b, 4, [tx_vote0, tx_vote1, tx_vote2]) update = ValidatorElection.approved_update(b, 4, [tx_vote0, tx_vote1, tx_vote2])
update_public_key = codecs.encode(update[0].pub_key.data, 'base64').decode().rstrip('\n') assert update
assert len(update) == 1 update_public_key = codecs.encode(update.pub_key.data, 'base64').decode().rstrip('\n')
assert update_public_key == public_key64 assert update_public_key == public_key64
b.store_bulk_transactions([tx_vote0, tx_vote1]) b.store_bulk_transactions([tx_vote0, tx_vote1])
update = ValidatorElection.get_validator_update(b, 4, [tx_vote2]) update = ValidatorElection.approved_update(b, 4, [tx_vote2])
update_public_key = codecs.encode(update[0].pub_key.data, 'base64').decode().rstrip('\n') assert update
assert len(update) == 1 update_public_key = codecs.encode(update.pub_key.data, 'base64').decode().rstrip('\n')
assert update_public_key == public_key64 assert update_public_key == public_key64
# remove validator # remove validator
@ -324,9 +325,10 @@ def test_get_validator_update(b, node_keys, node_key, ed25519_node_keys):
b.store_bulk_transactions([tx_vote0, tx_vote1]) b.store_bulk_transactions([tx_vote0, tx_vote1])
update = ValidatorElection.get_validator_update(b, 9, [tx_vote2]) update = ValidatorElection.approved_update(b, 9, [tx_vote2])
update_public_key = codecs.encode(update[0].pub_key.data, 'base64').decode().rstrip('\n') if update:
assert len(update) == 1 update_public_key = codecs.encode(update.pub_key.data, 'base64').decode().rstrip('\n')
assert update
assert update_public_key == public_key64 assert update_public_key == public_key64
# assert that the public key is not a part of the current validator set # assert that the public key is not a part of the current validator set
@ -348,10 +350,10 @@ def to_inputs(election, i, ed25519_node_keys):
def gen_vote(election, i, ed25519_node_keys): def gen_vote(election, i, ed25519_node_keys):
(input_i, votes_i, key_i) = to_inputs(election, i, ed25519_node_keys) (input_i, votes_i, key_i) = to_inputs(election, i, ed25519_node_keys)
election_pub_key = ValidatorElection.to_public_key(election.id) election_pub_key = ValidatorElection.to_public_key(election.id)
return ValidatorElectionVote.generate([input_i], return Vote.generate([input_i],
[([election_pub_key], votes_i)], [([election_pub_key], votes_i)],
election_id=election.id)\ election_id=election.id)\
.sign([key_i.private_key]) .sign([key_i.private_key])
def reset_validator_set(b, node_keys, height): def reset_validator_set(b, node_keys, height):
@ -360,4 +362,4 @@ def reset_validator_set(b, node_keys, height):
validators.append({'public_key': {'type': 'ed25519-base64', validators.append({'public_key': {'type': 'ed25519-base64',
'value': node_pub}, 'value': node_pub},
'voting_power': 10}) 'voting_power': 10})
b.store_validator_set(height, validators, 'election_id') b.store_validator_set(height, validators)

View File

@ -2,6 +2,7 @@
# SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0) # SPDX-License-Identifier: (Apache-2.0 AND CC-BY-4.0)
# Code is Apache-2.0 and docs are CC-BY-4.0 # Code is Apache-2.0 and docs are CC-BY-4.0
from argparse import Namespace from argparse import Namespace
from unittest.mock import patch
import pytest import pytest
@ -72,16 +73,17 @@ def test_upsert_validator_invalid_inputs_election(b_mock, new_validator, node_ke
election.validate(b_mock) election.validate(b_mock)
def test_upsert_validator_invalid_election(b_mock, new_validator, node_key, valid_election): @patch('bigchaindb.elections.election.uuid4', lambda: 'mock_uuid4')
def test_upsert_validator_invalid_election(b_mock, new_validator, node_key, fixed_seed_election):
voters = ValidatorElection.recipients(b_mock) voters = ValidatorElection.recipients(b_mock)
duplicate_election = ValidatorElection.generate([node_key.public_key], duplicate_election = ValidatorElection.generate([node_key.public_key],
voters, voters,
new_validator, None).sign([node_key.private_key]) new_validator, None).sign([node_key.private_key])
with pytest.raises(DuplicateTransaction): with pytest.raises(DuplicateTransaction):
valid_election.validate(b_mock, [duplicate_election]) fixed_seed_election.validate(b_mock, [duplicate_election])
b_mock.store_bulk_transactions([valid_election]) b_mock.store_bulk_transactions([fixed_seed_election])
with pytest.raises(DuplicateTransaction): with pytest.raises(DuplicateTransaction):
duplicate_election.validate(b_mock) duplicate_election.validate(b_mock)

View File

@ -10,7 +10,7 @@ def test_get_validators_endpoint(b, client):
'pub_key': {'data': '4E2685D9016126864733225BE00F005515200727FBAB1312FC78C8B76831255A', 'pub_key': {'data': '4E2685D9016126864733225BE00F005515200727FBAB1312FC78C8B76831255A',
'type': 'ed25519'}, 'type': 'ed25519'},
'voting_power': 10}] 'voting_power': 10}]
b.store_validator_set(23, validator_set, 'election_id') b.store_validator_set(23, validator_set)
res = client.get(VALIDATORS_ENDPOINT) res = client.get(VALIDATORS_ENDPOINT)
assert is_validator(res.json[0]) assert is_validator(res.json[0])