From b2839668d616d837d63b7b837c6a900ddbdebf80 Mon Sep 17 00:00:00 2001 From: Vanshdeep Singh Date: Tue, 14 Aug 2018 16:44:32 +0200 Subject: [PATCH] Problem: Election conclusion logic is spread accross multiple files Solution: Aggregate the logic in ValidatorElection class --- bigchaindb/backend/localmongodb/query.py | 11 ++-- bigchaindb/backend/query.py | 4 +- bigchaindb/core.py | 55 ++++--------------- bigchaindb/upsert_validator/__init__.py | 2 +- .../upsert_validator/validator_election.py | 33 ++++++++++- .../validator_election_vote.py | 2 +- .../upsert_validator/validator_utils.py | 37 +++++++++++++ tests/tendermint/test_core.py | 5 +- tests/upsert_validator/conftest.py | 8 +++ .../test_validator_election_vote.py | 45 ++++++++++++++- 10 files changed, 144 insertions(+), 58 deletions(-) create mode 100644 bigchaindb/upsert_validator/validator_utils.py diff --git a/bigchaindb/backend/localmongodb/query.py b/bigchaindb/backend/localmongodb/query.py index e6fe11ab..1f6f15ba 100644 --- a/bigchaindb/backend/localmongodb/query.py +++ b/bigchaindb/backend/localmongodb/query.py @@ -306,12 +306,15 @@ def get_validator_set(conn, height=None): @register_query(LocalMongoDBConnection) -def get_received_votes_for_election(conn, election_id, election_public_key): +def get_asset_tokens_for_public_keys(conn, asset_id, public_keys, operation=None): + query = {'outputs.public_keys': public_keys, + 'asset.id': asset_id} + if operation: + query['operation'] = operation + cursor = conn.run( conn.collection('transactions').aggregate([ - {'$match': {'outputs.public_keys': election_public_key, - 'operation': 'VALIDATOR_ELECTION_VOTE', - 'asset.id': election_id}}, + {'$match': query}, {'$project': {'_id': False}} ])) return cursor diff --git a/bigchaindb/backend/query.py b/bigchaindb/backend/query.py index d2083be5..1a0146cb 100644 --- a/bigchaindb/backend/query.py +++ b/bigchaindb/backend/query.py @@ -370,11 +370,13 @@ def get_validator_set(conn, height): @singledispatch -def get_received_votes_for_election(connection, election_id, election_public_key): +def get_asset_tokens_for_public_keys(connection, asset_id, + public_keys, operation): """Retrieve a list of `VALIDATOR_ELECTION_VOTE`s `txids` that are owned by `owner`. Args: election_id (str): Id of the election. election_public_key (str): base58 encoded public key of the election + operation: filter transaction based on `operation` Returns: Iterator of transaction that list given owner in conditions. """ diff --git a/bigchaindb/core.py b/bigchaindb/core.py index bdc852cf..9d9d7513 100644 --- a/bigchaindb/core.py +++ b/bigchaindb/core.py @@ -1,7 +1,6 @@ """This module contains all the goodness to integrate BigchainDB with Tendermint.""" import logging -import codecs from abci.application import BaseApplication from abci.types_pb2 import ( @@ -12,16 +11,15 @@ from abci.types_pb2 import ( ResponseDeliverTx, ResponseEndBlock, ResponseCommit, - Validator, - PubKey ) from bigchaindb import BigchainDB from bigchaindb.tendermint_utils import (decode_transaction, - public_key_to_base64, calculate_hash) from bigchaindb.lib import Block, PreCommitState from bigchaindb.backend.query import PRE_COMMIT_ID +from bigchaindb.upsert_validator import ValidatorElection +import bigchaindb.upsert_validator.validator_utils as vutils CodeTypeOk = 0 @@ -47,7 +45,7 @@ class App(BaseApplication): def init_chain(self, genesis): """Initialize chain with block of height 0""" - validator_set = [decode_validator(v) for v in genesis.validators] + validator_set = [vutils.decode_validator(v) for v in genesis.validators] block = Block(app_hash='', height=0, transactions=[]) self.bigchaindb.store_block(block._asdict()) self.bigchaindb.store_validator_set(1, validator_set) @@ -135,13 +133,14 @@ class App(BaseApplication): # TODO: calculate if an election has concluded # NOTE: ensure the local validator set is updated - validator_updates = self.bigchaindb.get_validator_update(self.block_transactions) + validator_updates = ValidatorElection.get_validator_update(self.bigchaindb, + self.new_height, + self.block_transactions) + # if validator_updates: + # validator_set = new_validator_set(self.bigchaindb, self.new_height, validator_updates) + # self.bigchaindb.store_validator_set(self.new_height+1, validator_set) - if validator_updates: - validator_set = new_validator_set(self.bigchaindb, self.new_height, validator_updates) - self.bigchaindb.store_validator_set(self.new_height+1, validator_set) - - validator_updates = [encode_validator(v) for v in validator_updates] + # validator_updates = [vutils.encode_validator(v) for v in validator_updates] # Store pre-commit state to recover in case there is a crash # during `commit` @@ -172,37 +171,3 @@ class App(BaseApplication): self.block_txn_ids) logger.benchmark('COMMIT_BLOCK, height:%s', self.new_height) return ResponseCommit(data=data) - - -def encode_validator(v): - ed25519_public_key = v['public_key'] - # NOTE: tendermint expects public to be encoded in go-amino format - pub_key = PubKey(type='ed25519', - data=bytes.fromhex(ed25519_public_key)) - - return Validator(pub_key=pub_key, - address=b'', - power=v['power']) - - -def decode_validator(v): - return {'pub_key': {'type': v.pub_key.type, - 'data': codecs.encode(v.pub_key.data, 'base64').decode().rstrip('\n')}, - 'voting_power': v.power} - - -def new_validator_set(bigchain, height, updates): - validators = bigchain.get_validators(height) - validators_dict = {} - for v in validators: - validators_dict[v['pub_key']['data']] = v - - updates_dict = {} - for u in updates: - public_key64 = public_key_to_base64(u['public_key']) - updates_dict[public_key64] = {'pub_key': {'type': 'ed25519', - 'data': public_key64}, - 'voting_power': u['power']} - - new_validators_dict = {**validators_dict, **updates_dict} - return list(new_validators_dict.values()) diff --git a/bigchaindb/upsert_validator/__init__.py b/bigchaindb/upsert_validator/__init__.py index a318e861..c19ecc1d 100644 --- a/bigchaindb/upsert_validator/__init__.py +++ b/bigchaindb/upsert_validator/__init__.py @@ -1,3 +1,3 @@ -from bigchaindb.upsert_validator.validator_election import ValidatorElection # noqa from bigchaindb.upsert_validator.validator_election_vote import ValidatorElectionVote # noqa +from bigchaindb.upsert_validator.validator_election import ValidatorElection # noqa diff --git a/bigchaindb/upsert_validator/validator_election.py b/bigchaindb/upsert_validator/validator_election.py index f40bbfaf..89981ea3 100644 --- a/bigchaindb/upsert_validator/validator_election.py +++ b/bigchaindb/upsert_validator/validator_election.py @@ -14,6 +14,8 @@ 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) class ValidatorElection(Transaction): @@ -83,7 +85,7 @@ class ValidatorElection(Transaction): bigchain (BigchainDB): an instantiated bigchaindb.lib.BigchainDB object. Returns: - `True` if the election is valid + ValidatorElection object Raises: ValidationError: If the election is invalid @@ -174,7 +176,10 @@ class ValidatorElection(Transaction): 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_received_votes_for_election(bigchain.connection, self.id, election_pk)) + txns = list(backend.query.get_asset_tokens_for_public_keys(bigchain.connection, + self.id, + [election_pk], + 'VALIDATOR_ELECTION_VOTE')) return self.count_votes(election_pk, txns) @classmethod @@ -200,3 +205,27 @@ class ValidatorElection(Transaction): return election return False + + @classmethod + def get_validator_update(cls, bigchain, new_height, txns): + votes = {} + for txn in txns: + if isinstance(txn, ValidatorElectionVote): + election_id = txn.asset['id'] + election_votes = votes.get(election_id, []) + election_votes.append(txn) + votes[election_id] = election_votes + + election = cls.conclude(bigchain, election_id, election_votes) + # 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, + new_height, validator_updates) + + bigchain.store_validator_set(new_height+1, updated_validator_set) + return [encode_validator(election.asset['data'])] + return [] diff --git a/bigchaindb/upsert_validator/validator_election_vote.py b/bigchaindb/upsert_validator/validator_election_vote.py index c25b7504..39ad7f0f 100644 --- a/bigchaindb/upsert_validator/validator_election_vote.py +++ b/bigchaindb/upsert_validator/validator_election_vote.py @@ -24,7 +24,7 @@ class ValidatorElectionVote(Transaction): bigchain (BigchainDB): an instantiated bigchaindb.lib.BigchainDB object. Returns: - `True` if the election vote is valid + ValidatorElectionVote object Raises: ValidationError: If the election vote is invalid diff --git a/bigchaindb/upsert_validator/validator_utils.py b/bigchaindb/upsert_validator/validator_utils.py new file mode 100644 index 00000000..7cb924d8 --- /dev/null +++ b/bigchaindb/upsert_validator/validator_utils.py @@ -0,0 +1,37 @@ +import codecs + +from abci.types_pb2 import (Validator, + PubKey) +from bigchaindb.tendermint_utils import public_key_to_base64 + + +def encode_validator(v): + ed25519_public_key = v['public_key'] + # NOTE: tendermint expects public to be encoded in go-amino format + pub_key = PubKey(type='ed25519', + data=bytes.fromhex(ed25519_public_key)) + return Validator(pub_key=pub_key, + address=b'', + power=v['power']) + + +def decode_validator(v): + return {'pub_key': {'type': v.pub_key.type, + 'data': codecs.encode(v.pub_key.data, 'base64').decode().rstrip('\n')}, + 'voting_power': v.power} + + +def new_validator_set(validators, height, updates): + validators_dict = {} + for v in validators: + validators_dict[v['pub_key']['data']] = v + + updates_dict = {} + for u in updates: + public_key64 = public_key_to_base64(u['public_key']) + updates_dict[public_key64] = {'pub_key': {'type': 'ed25519', + 'data': public_key64}, + 'voting_power': u['power']} + + new_validators_dict = {**validators_dict, **updates_dict} + return list(new_validators_dict.values()) diff --git a/tests/tendermint/test_core.py b/tests/tendermint/test_core.py index 5909f003..25e58257 100644 --- a/tests/tendermint/test_core.py +++ b/tests/tendermint/test_core.py @@ -8,7 +8,8 @@ from abci.types_pb2 import ( from bigchaindb.core import (CodeTypeOk, CodeTypeError, - new_validator_set) + ) +from bigchaindb.upsert_validator.validator_utils import new_validator_set from bigchaindb.tendermint_utils import public_key_to_base64 @@ -233,7 +234,7 @@ def test_new_validator_set(b): validators = [node1] updates = [node1_new_power, node2] b.store_validator_set(1, validators) - updated_validator_set = new_validator_set(b, 1, updates) + updated_validator_set = new_validator_set(b.get_validators(1), 1, updates) updated_validators = [] for u in updates: diff --git a/tests/upsert_validator/conftest.py b/tests/upsert_validator/conftest.py index 54921b1a..d6293b14 100644 --- a/tests/upsert_validator/conftest.py +++ b/tests/upsert_validator/conftest.py @@ -40,3 +40,11 @@ def valid_election(b_mock, node_key, new_validator): return ValidatorElection.generate([node_key.public_key], voters, new_validator, None).sign([node_key.private_key]) + + +@pytest.fixture +def valid_election_b(b, node_key, new_validator): + voters = ValidatorElection.recipients(b) + return ValidatorElection.generate([node_key.public_key], + voters, + new_validator, None).sign([node_key.private_key]) diff --git a/tests/upsert_validator/test_validator_election_vote.py b/tests/upsert_validator/test_validator_election_vote.py index 185107db..64a7b8f4 100644 --- a/tests/upsert_validator/test_validator_election_vote.py +++ b/tests/upsert_validator/test_validator_election_vote.py @@ -1,5 +1,7 @@ import pytest +import codecs +from bigchaindb.tendermint_utils import public_key_to_base64 from bigchaindb.upsert_validator import ValidatorElection, ValidatorElectionVote from bigchaindb.common.exceptions import AmountError from bigchaindb.common.crypto import generate_key_pair @@ -194,7 +196,7 @@ def test_valid_election_conclude(b_mock, valid_election, ed25519_node_keys): @pytest.mark.tendermint @pytest.mark.bdb -def test_get_validator_update(b_mock, valid_election, ed25519_node_keys): +def test_get_validator_update_conclude(b_mock, valid_election, ed25519_node_keys): # store election b_mock.store_bulk_transactions([valid_election]) @@ -218,7 +220,6 @@ def test_get_validator_update(b_mock, valid_election, ed25519_node_keys): @pytest.mark.abci def test_upsert_validator(b, node_key, node_keys, new_validator, ed25519_node_keys): - from bigchaindb.tendermint_utils import public_key_to_base64 import time import requests @@ -269,6 +270,37 @@ def test_upsert_validator(b, node_key, node_keys, new_validator, ed25519_node_ke assert (public_key64 in validator_pub_keys) +@pytest.mark.tendermint +@pytest.mark.bdb +def test_get_validator_update(b, node_keys, node_key, ed25519_node_keys): + reset_validator_set(b, node_keys, 1) + + power = 1 + public_key = '9B3119650DF82B9A5D8A12E38953EA47475C09F0C48A4E6A0ECE182944B24403' + public_key64 = public_key_to_base64(public_key) + new_validator = {'public_key': public_key, + 'node_id': 'some_node_id', + 'power': power} + voters = ValidatorElection.recipients(b) + election = ValidatorElection.generate([node_key.public_key], + voters, + new_validator).sign([node_key.private_key]) + # store election + b.store_bulk_transactions([election]) + + tx_vote0 = gen_vote(election, 0, ed25519_node_keys) + tx_vote1 = gen_vote(election, 1, ed25519_node_keys) + tx_vote2 = gen_vote(election, 2, ed25519_node_keys) + + assert ValidatorElection.conclude(b, election.id, [tx_vote0, tx_vote1, tx_vote2]) + + update = ValidatorElection.get_validator_update(b, 4, [tx_vote0, tx_vote1, tx_vote2]) + + assert len(update) == 1 + update_public_key = codecs.encode(update[0].pub_key.data, 'base64').decode().rstrip('\n') + assert update_public_key == public_key64 + + # ============================================================================ # Helper functions # ============================================================================ @@ -287,3 +319,12 @@ def gen_vote(election, i, ed25519_node_keys): [([election_pub_key], votes_i)], election_id=election.id)\ .sign([key_i.private_key]) + + +def reset_validator_set(b, node_keys, height): + validators = [] + for (node_pub, _) in node_keys.items(): + validators.append({'pub_key': {'type': 'ed25519', + 'data': node_pub}, + 'voting_power': 10}) + b.store_validator_set(height, validators)