Merge branch 'abstract_election_class' into generalize-election-management-commands

This commit is contained in:
z-bowen 2018-09-05 11:53:46 +02:00
commit abc74579cd
20 changed files with 406 additions and 325 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.common.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.VOTE, Vote)

View File

@ -281,6 +281,18 @@ def store_validator_set(conn, validators_update):
) )
@register_query(LocalMongoDBConnection)
def store_election(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)
@ -143,6 +144,14 @@ def create_abci_chains_indexes(conn, dbname):
name='height', name='height',
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,)
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',

View File

@ -351,6 +351,13 @@ def store_validator_set(conn, validator_update):
raise NotImplementedError raise NotImplementedError
@singledispatch
def store_election(conn, validator_update):
"""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.common.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
@ -175,7 +176,7 @@ 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)

View File

@ -0,0 +1,249 @@
# 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 bigchaindb import backend
from bigchaindb.common.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
# the model for votes issued by the election
VOTE_TYPE = Vote
# 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['pub_key']['data']))
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:
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)
# 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):
(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(), skip_id=True)
return election
@classmethod
def validate_schema(cls, tx, skip_id=False):
"""Validate the election transaction. Since `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)
@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') == cls.VOTE_TYPE.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_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(cls, bigchain, election, height):
bigchain.store_election(height, election)
@classmethod
def is_approved(cls, bigchain, new_height, txns):
votes = {}
for txn in txns:
if not isinstance(txn, cls.VOTE_TYPE):
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(bigchain, election, new_height)
return cls.on_approval(bigchain, election, new_height)
return []
@classmethod
def on_approval(cls, bigchain, election, new_height):
raise NotImplementedError

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

@ -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: "OPERATION"
outputs: outputs:
type: array type: array
items: items:

View File

@ -6,16 +6,16 @@ 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' VOTE = '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 = VOTE
ALLOWED_OPERATIONS = (VALIDATOR_ELECTION_VOTE,) ALLOWED_OPERATIONS = (VOTE,)
def validate(self, bigchain, current_transactions=[]): def validate(self, bigchain, current_transactions=[]):
"""Validate election vote transaction """Validate election vote transaction
@ -28,7 +28,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 object
Raises: Raises:
ValidationError: If the election vote is invalid ValidationError: If the election vote is invalid
@ -39,20 +39,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.VOTE, {'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(TX_SCHEMA_VOTE, 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

@ -209,7 +209,7 @@ 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_updates = ValidatorElection.is_approved(self.bigchaindb,
self.new_height, self.new_height,
self.block_transactions) self.block_transactions)

View File

@ -433,13 +433,10 @@ 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)
@ -483,6 +480,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(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(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,232 +2,47 @@
# 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.common.election import Election
from bigchaindb import backend
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 (_validate_schema, from bigchaindb.common.schema import (_validate_schema,
TX_SCHEMA_VALIDATOR_ELECTION, TX_SCHEMA_VALIDATOR_ELECTION)
TX_SCHEMA_COMMON,
TX_SCHEMA_CREATE)
from . import ValidatorElectionVote
from .validator_utils import (new_validator_set, encode_validator) from .validator_utils import (new_validator_set, encode_validator)
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:
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['pub_key']['data']))
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
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 @classmethod
def validate_schema(cls, tx, skip_id=False): def validate_schema(cls, tx, skip_id=False):
"""Validate the validator election transaction. Since `VALIDATOR_ELECTION` extends `CREATE` """Validate the validator election transaction. Since `VALIDATOR_ELECTION` extends `ELECTION`
transaction, all the validations for `CREATE` transaction should be inherited transaction, all the validations for `ELECTION` transaction are covered by `super`
""" """
if not skip_id:
cls.validate_id(tx) super(ValidatorElection, cls).validate_schema(tx, skip_id=skip_id)
_validate_schema(TX_SCHEMA_COMMON, tx)
_validate_schema(TX_SCHEMA_CREATE, tx)
_validate_schema(TX_SCHEMA_VALIDATOR_ELECTION, tx) _validate_schema(TX_SCHEMA_VALIDATOR_ELECTION, tx)
@classmethod @classmethod
def create(cls, tx_signers, recipients, metadata=None, asset=None): def on_approval(cls, bigchain, election, new_height):
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') == '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 # The new validator set comes into effect from height = new_height+1
validator_updates = [election.asset['data']] validator_updates = [election.asset['data']]
curr_validator_set = bigchain.get_validators(new_height) curr_validator_set = bigchain.get_validators(new_height)
@ -237,22 +52,3 @@ class ValidatorElection(Transaction):
updated_validator_set = [v for v in updated_validator_set if v['voting_power'] > 0] 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) bigchain.store_validator_set(new_height+1, updated_validator_set, election.id)
return [encode_validator(election.asset['data'])] return [encode_validator(election.asset['data'])]
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

@ -24,7 +24,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()
@ -49,6 +49,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
@ -81,7 +84,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',
} }
@ -120,6 +123,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,7 +366,7 @@ 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.is_approved()
assert updates == [] assert updates == []

View File

@ -164,11 +164,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.is_approved()
assert updates == [validator_update['validator']] assert updates == [validator_update['validator']]
b.delete_validator_update() b.delete_validator_update()
assert b.get_validator_update() == [] assert b.is_approved() == []
@pytest.mark.bdb @pytest.mark.bdb

View File

@ -4,7 +4,7 @@
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
@ -62,12 +62,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,
'height': 2,
'election_id': ongoing_election.id} 'election_id': ongoing_election.id}
query.store_validator_set(b.connection, validator_update) query.store_election(b.connection, election_result)
return ongoing_election return ongoing_election
@ -91,7 +89,7 @@ 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])

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.common.vote import Vote
from tests.utils import generate_block from tests.utils import generate_block
pytestmark = [pytest.mark.execute] pytestmark = [pytest.mark.execute]
@ -27,7 +28,7 @@ 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])
@ -48,7 +49,7 @@ 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])
@ -66,7 +67,7 @@ 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])
@ -77,14 +78,14 @@ 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])
@ -103,7 +104,7 @@ 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])
@ -125,7 +126,7 @@ 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])
@ -136,7 +137,7 @@ 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])
@ -147,7 +148,7 @@ 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])
@ -299,17 +300,17 @@ 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 ValidatorElection.is_approved(b, 4, [tx_vote0]) == []
assert ValidatorElection.get_validator_update(b, 4, [tx_vote0, tx_vote1]) == [] assert ValidatorElection.is_approved(b, 4, [tx_vote0, tx_vote1]) == []
update = ValidatorElection.get_validator_update(b, 4, [tx_vote0, tx_vote1, tx_vote2]) update = ValidatorElection.is_approved(b, 4, [tx_vote0, tx_vote1, tx_vote2])
update_public_key = codecs.encode(update[0].pub_key.data, 'base64').decode().rstrip('\n') update_public_key = codecs.encode(update[0].pub_key.data, 'base64').decode().rstrip('\n')
assert len(update) == 1 assert len(update) == 1
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.is_approved(b, 4, [tx_vote2])
update_public_key = codecs.encode(update[0].pub_key.data, 'base64').decode().rstrip('\n') update_public_key = codecs.encode(update[0].pub_key.data, 'base64').decode().rstrip('\n')
assert len(update) == 1 assert len(update) == 1
assert update_public_key == public_key64 assert update_public_key == public_key64
@ -332,7 +333,7 @@ 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.is_approved(b, 9, [tx_vote2])
update_public_key = codecs.encode(update[0].pub_key.data, 'base64').decode().rstrip('\n') update_public_key = codecs.encode(update[0].pub_key.data, 'base64').decode().rstrip('\n')
assert len(update) == 1 assert len(update) == 1
assert update_public_key == public_key64 assert update_public_key == public_key64
@ -356,7 +357,7 @@ 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])