From 7a0b474d118fa7a1f57c630b5fe57d1ad9261a05 Mon Sep 17 00:00:00 2001 From: Zachary Bowen Date: Thu, 30 Aug 2018 10:47:37 +0200 Subject: [PATCH] Problem: Users want to know upsert-validator election status. Solution: Introduce the `upsert-validator show` command. Soon to be re-implemented via storing and querying identifiers of concluded elections. --- Dockerfile-dev | 2 +- bigchaindb/commands/bigchaindb.py | 38 +++++++++- .../upsert_validator/validator_election.py | 57 ++++++++++++++- .../upsert_validator/validator_utils.py | 2 +- .../source/server-reference/bigchaindb-cli.md | 16 +++++ tests/commands/test_commands.py | 42 ++++------- tests/commands/test_utils.py | 6 ++ tests/tendermint/test_core.py | 2 +- tests/upsert_validator/conftest.py | 50 +++++++++++++ .../test_validator_election.py | 71 +++++++++++++++++-- 10 files changed, 248 insertions(+), 38 deletions(-) diff --git a/Dockerfile-dev b/Dockerfile-dev index 4425e0d2..9f5a4f43 100644 --- a/Dockerfile-dev +++ b/Dockerfile-dev @@ -33,4 +33,4 @@ RUN mkdir -p /usr/src/app COPY . /usr/src/app/ WORKDIR /usr/src/app RUN pip install --no-cache-dir --process-dependency-links -e .[dev] -RUN bigchaindb -y configure +RUN bigchaindb -y configure \ No newline at end of file diff --git a/bigchaindb/commands/bigchaindb.py b/bigchaindb/commands/bigchaindb.py index 1a970f88..19cb0e2d 100644 --- a/bigchaindb/commands/bigchaindb.py +++ b/bigchaindb/commands/bigchaindb.py @@ -27,8 +27,7 @@ from bigchaindb.commands import utils from bigchaindb.commands.utils import (configure_bigchaindb, input_on_stderr) from bigchaindb.log import setup_logging -from bigchaindb.tendermint_utils import public_key_from_base64 - +from bigchaindb.tendermint_utils import public_key_from_base64, public_key_to_base64 logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) @@ -191,6 +190,35 @@ def run_upsert_validator_approve(args, bigchain): return False +def run_upsert_validator_show(args, bigchain): + """Retrieves information about an upsert-validator election + + :param args: dict + args = { + 'election_id': the transaction_id for an election (str) + } + :param bigchain: an instance of BigchainDB + """ + + election = bigchain.get_transaction(args.election_id) + if not election: + logger.error(f'No election found with election_id {args.election_id}') + return + + new_validator = election.asset['data'] + + public_key = public_key_to_base64(new_validator['public_key']) + power = new_validator['power'] + node_id = new_validator['node_id'] + status = election.get_status(bigchain) + + response = f'public_key={public_key}\npower={power}\nnode_id={node_id}\nstatus={status}' + + logger.info(response) + + return response + + def _run_init(): bdb = bigchaindb.BigchainDB() @@ -320,6 +348,12 @@ def create_parser(): dest='sk', help='Path to the private key of the election initiator.') + show_election_parser = validator_subparser.add_parser('show', + help='Provides information about an election.') + + show_election_parser.add_argument('election_id', + help='The transaction id of the election you wish to query.') + # parsers for showing/exporting config values subparsers.add_parser('show-config', help='Show the current configuration') diff --git a/bigchaindb/upsert_validator/validator_election.py b/bigchaindb/upsert_validator/validator_election.py index f007c38b..89e60335 100644 --- a/bigchaindb/upsert_validator/validator_election.py +++ b/bigchaindb/upsert_validator/validator_election.py @@ -5,6 +5,7 @@ import base58 from bigchaindb import backend +from bigchaindb.backend.localmongodb.query import get_asset_tokens_for_public_key from bigchaindb.common.exceptions import (InvalidSignature, MultipleInputsError, InvalidProposer, @@ -29,6 +30,11 @@ class ValidatorElection(Transaction): # by renaming CREATE to VALIDATOR_ELECTION CREATE = VALIDATOR_ELECTION ALLOWED_OPERATIONS = (VALIDATOR_ELECTION,) + # Election Statuses: + ONGOING = 'ongoing' + CONCLUDED = 'concluded' + INCONCLUSIVE = 'inconclusive' + ELECTION_THRESHOLD = 2 / 3 def __init__(self, operation, asset, inputs, outputs, metadata=None, version=None, hash_id=None): @@ -218,9 +224,58 @@ class ValidatorElection(Transaction): 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) + 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) return [encode_validator(election.asset['data'])] return [] + + def _vote_ratio(self, bigchain, height): + cast_votes = self._get_vote_ids(bigchain) + votes = [(tx['outputs'][0]['amount'], bigchain.get_block_containing_tx(tx['id'])[0]) for tx in cast_votes] + votes_cast = [int(vote[0]) for vote in votes if vote[1] <= height] + total_votes_cast = sum(votes_cast) + total_votes = sum([voter.amount for voter in self.outputs]) + vote_ratio = total_votes_cast/total_votes + return vote_ratio + + def _get_vote_ids(self, bigchain): + election_key = self.to_public_key(self.id) + votes = get_asset_tokens_for_public_key(bigchain.connection, self.id, election_key) + return votes + + def initial_height(self, bigchain): + heights = bigchain.get_block_containing_tx(self.id) + initial_height = 0 + if len(heights) != 0: + initial_height = min(bigchain.get_block_containing_tx(self.id)) + return initial_height + + def get_status(self, bigchain, height=None): + + initial_validators = self.get_validators(bigchain, height=self.initial_height(bigchain)) + + # get all heights where a vote was cast + vote_heights = set([bigchain.get_block_containing_tx(tx['id'])[0] for tx in self._get_vote_ids(bigchain)]) + + # find the least height where the vote succeeds + confirmation_height = None + confirmed_heights = [h for h in vote_heights if self._vote_ratio(bigchain, h) > self.ELECTION_THRESHOLD] + if height: + confirmed_heights = [h for h in confirmed_heights if h <= height] + if len(confirmed_heights) > 0: + confirmation_height = min(confirmed_heights) + + # get the validator set at the confirmation height/current height + if confirmation_height: + final_validators = self.get_validators(bigchain, height=confirmation_height) + else: + final_validators = self.get_validators(bigchain) + + if initial_validators != final_validators: + return self.INCONCLUSIVE + elif confirmation_height: + return self.CONCLUDED + else: + return self.ONGOING diff --git a/bigchaindb/upsert_validator/validator_utils.py b/bigchaindb/upsert_validator/validator_utils.py index 7cb924d8..75c7baf5 100644 --- a/bigchaindb/upsert_validator/validator_utils.py +++ b/bigchaindb/upsert_validator/validator_utils.py @@ -21,7 +21,7 @@ def decode_validator(v): 'voting_power': v.power} -def new_validator_set(validators, height, updates): +def new_validator_set(validators, updates): validators_dict = {} for v in validators: validators_dict[v['pub_key']['data']] = v diff --git a/docs/server/source/server-reference/bigchaindb-cli.md b/docs/server/source/server-reference/bigchaindb-cli.md index 80f1656f..80dbccdb 100644 --- a/docs/server/source/server-reference/bigchaindb-cli.md +++ b/docs/server/source/server-reference/bigchaindb-cli.md @@ -139,3 +139,19 @@ $ bigchaindb upsert-validator approve 04a067582cf03eba2b53b82e4adb5ece424474cbd4 ``` If the command succeeds a message will be returned stating that the vote was submitted successfully. Once a proposal has been approved by sufficent validators (more than `2/3` of the total voting power) then the proposed change is applied to the network. For example, consider a network wherein the total power is `90` then the proposed changed applied only after `60` (`2/3 * 90`) have been received. + +#### upsert-validator show + +Retrieves information about an election initiated by `upsert-validator new`. + +Below is the command line syntax and the return value, + +```bash +$ bigchaindb upsert-validator show ELECTION_ID +public_key= +power= +node_id= +status= +``` + +The `public_key`, `power`, and `node_id` are the same values used in the `upsert-validator new` command that originally triggered the election. `status` takes three possible values, `ongoing`, if the election has not yet reached a 2/3 majority, `concluded`, if the election reached the 2/3 majority needed to pass, or `inconclusive`, if the validator set changed while the election was in process, rendering it undecidable. \ No newline at end of file diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index b3f8e84c..7b2f8e28 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -347,34 +347,20 @@ class MockResponse(): return {'result': {'latest_block_height': self.height}} -# @pytest.mark.execute -# @patch('bigchaindb.lib.BigchainDB.get_validators') -# @pytest.mark.abci -@pytest.mark.skip -def test_upsert_validator_new_with_tendermint(b, priv_validator_path, user_sk, monkeypatch): - """WIP: Will be fixed and activated in the next PR - """ +@pytest.mark.abci +def test_upsert_validator_new_with_tendermint(b, priv_validator_path, user_sk, validators): from bigchaindb.commands.bigchaindb import run_upsert_validator_new - import time - time.sleep(3) + new_args = Namespace(action='new', + public_key='8eJ8q9ZQpReWyQT5aFCiwtZ5wDZC4eDnCen88p3tQ6ie', + power=1, + node_id='unique_node_id_for_test_upsert_validator_new_with_tendermint', + sk=priv_validator_path, + config={}) - # b.get_validators = mock_get - # mock_get_validators = mock_get - # monkeypatch.setattr('requests.get', mock_get) + election_id = run_upsert_validator_new(new_args, b) - proposer_key = b.get_validators()[0]['pub_key']['value'] - - args = Namespace(action='new', - public_key=proposer_key, - power=1, - node_id='12345', - sk=priv_validator_path, - config={}) - resp = run_upsert_validator_new(args, b) - time.sleep(3) - - assert b.get_transaction(resp) + assert b.get_transaction(election_id) @pytest.mark.tendermint @@ -386,7 +372,7 @@ def test_upsert_validator_new_without_tendermint(caplog, b, priv_validator_path, b.store_bulk_transactions([tx]) return (202, '') - b.get_validators = mock_get + b.get_validators = mock_get_validators b.write_transaction = mock_write args = Namespace(action='new', @@ -430,7 +416,7 @@ def test_upsert_validator_new_election_invalid_power(caplog, b, priv_validator_p return (400, '') b.write_transaction = mock_write - b.get_validators = mock_get + b.get_validators = mock_get_validators args = Namespace(action='new', public_key='CJxdItf4lz2PwEf4SmYNAu/c/VpmX39JEgC5YpH7fxg=', power=10, @@ -533,7 +519,7 @@ def test_upsert_validator_approve_called_with_bad_key(caplog, b, bad_validator_p 'the eligible voters in this election.' -def mock_get(height): +def mock_get_validators(height): keys = node_keys() pub_key = list(keys.keys())[0] return [ @@ -550,7 +536,7 @@ def call_election(b, new_validator, node_key): return (202, '') # patch the validator set. We now have one validator with power 10 - b.get_validators = mock_get + b.get_validators = mock_get_validators b.write_transaction = mock_write # our voters is a list of length 1, populated from our mocked validator diff --git a/tests/commands/test_utils.py b/tests/commands/test_utils.py index 6b00cd30..11ff41a8 100644 --- a/tests/commands/test_utils.py +++ b/tests/commands/test_utils.py @@ -72,6 +72,12 @@ def test_configure_bigchaindb_configures_bigchaindb(): logging.CRITICAL) ))) def test_configure_bigchaindb_logging(log_level): + # TODO: See following comment: + # This is a dirty test. If a test *preceding* this test makes use of the logger, and then another test *after* this + # test also makes use of the logger, somehow we get logger.disabled == True, and the later test fails. We need to + # either engineer this somehow to leave the test env in the same state as it finds it, or make an assessment + # whether or not we even need this test, and potentially just remove it. + from bigchaindb.commands.utils import configure_bigchaindb @configure_bigchaindb diff --git a/tests/tendermint/test_core.py b/tests/tendermint/test_core.py index e698d45a..6887264c 100644 --- a/tests/tendermint/test_core.py +++ b/tests/tendermint/test_core.py @@ -238,7 +238,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.get_validators(1), 1, updates) + updated_validator_set = new_validator_set(b.get_validators(1), updates) updated_validators = [] for u in updates: diff --git a/tests/upsert_validator/conftest.py b/tests/upsert_validator/conftest.py index 49b41b0f..4a1b6925 100644 --- a/tests/upsert_validator/conftest.py +++ b/tests/upsert_validator/conftest.py @@ -4,6 +4,9 @@ import pytest +from bigchaindb import ValidatorElectionVote +from bigchaindb.backend.localmongodb import query +from bigchaindb.lib import Block from bigchaindb.upsert_validator import ValidatorElection @@ -41,3 +44,50 @@ def valid_election_b(b, node_key, new_validator): return ValidatorElection.generate([node_key.public_key], voters, new_validator, None).sign([node_key.private_key]) + + +@pytest.fixture +def ongoing_election(b, valid_election, ed25519_node_keys): + b.store_bulk_transactions([valid_election]) + block_1 = Block(app_hash='hash_1', height=1, transactions=[valid_election.id]) + vote_0 = vote(valid_election, 0, ed25519_node_keys, b) + vote_1 = vote(valid_election, 1, ed25519_node_keys, b) + block_2 = Block(app_hash='hash_2', height=2, transactions=[vote_0.id, vote_1.id]) + b.store_block(block_1._asdict()) + b.store_block(block_2._asdict()) + return valid_election + + +@pytest.fixture +def concluded_election(b, ongoing_election, ed25519_node_keys): + vote_2 = vote(ongoing_election, 2, ed25519_node_keys, b) + block_4 = Block(app_hash='hash_4', height=4, transactions=[vote_2.id]) + b.store_block(block_4._asdict()) + return ongoing_election + + +@pytest.fixture +def inconclusive_election(b, concluded_election, new_validator): + validators = b.get_validators(height=1) + validators[0]['voting_power'] = 15 + validator_update = {'validators': validators, + 'height': 3} + + query.store_validator_set(b.connection, validator_update) + return concluded_election + + +def vote(election, voter, keys, b): + election_input = election.to_inputs()[voter] + votes = election.outputs[voter].amount + public_key = election_input.owners_before[0] + key = keys[public_key] + + election_pub_key = ValidatorElection.to_public_key(election.id) + + v = ValidatorElectionVote.generate([election_input], + [([election_pub_key], votes)], + election_id=election.id)\ + .sign([key.private_key]) + b.store_bulk_transactions([v]) + return v diff --git a/tests/upsert_validator/test_validator_election.py b/tests/upsert_validator/test_validator_election.py index d44d9f37..d09d6403 100644 --- a/tests/upsert_validator/test_validator_election.py +++ b/tests/upsert_validator/test_validator_election.py @@ -1,9 +1,11 @@ # 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 +from argparse import Namespace import pytest +from bigchaindb.tendermint_utils import public_key_to_base64 from bigchaindb.upsert_validator import ValidatorElection from bigchaindb.common.exceptions import (DuplicateTransaction, UnequalValidatorSet, @@ -57,11 +59,8 @@ def test_upsert_validator_invalid_inputs_election(b_mock, new_validator, node_ke election.validate(b_mock) -def test_upsert_validator_invalid_election(b_mock, new_validator, node_key): +def test_upsert_validator_invalid_election(b_mock, new_validator, node_key, valid_election): voters = ValidatorElection.recipients(b_mock) - valid_election = ValidatorElection.generate([node_key.public_key], - voters, - new_validator, None).sign([node_key.private_key]) duplicate_election = ValidatorElection.generate([node_key.public_key], voters, new_validator, None).sign([node_key.private_key]) @@ -95,3 +94,67 @@ def test_upsert_validator_invalid_election(b_mock, new_validator, node_key): with pytest.raises(UnequalValidatorSet): tx_election.validate(b_mock) + + +def test_get_status_ongoing(b, ongoing_election, new_validator): + status = ValidatorElection.ONGOING + resp = ongoing_election.get_status(b) + assert resp == status + + +def test_get_status_concluded(b, concluded_election, new_validator): + status = ValidatorElection.CONCLUDED + resp = concluded_election.get_status(b) + assert resp == status + + +def test_get_status_inconclusive(b, inconclusive_election, new_validator): + def custom_mock_get_validators(height): + if height >= 3: + return [{'pub_key': {'data': 'zL/DasvKulXZzhSNFwx4cLRXKkSM9GPK7Y0nZ4FEylM=', + 'type': 'AC26791624DE60'}, + 'voting_power': 15}, + {'pub_key': {'data': 'GIijU7GBcVyiVUcB0GwWZbxCxdk2xV6pxdvL24s/AqM=', + 'type': 'AC26791624DE60'}, + 'voting_power': 7}, + {'pub_key': {'data': 'JbfwrLvCVIwOPm8tj8936ki7IYbmGHjPiKb6nAZegRA=', + 'type': 'AC26791624DE60'}, + 'voting_power': 10}, + {'pub_key': {'data': 'PecJ58SaNRsWJZodDmqjpCWqG6btdwXFHLyE40RYlYM=', + 'type': 'AC26791624DE60'}, + 'voting_power': 8}] + else: + return [{'pub_key': {'data': 'zL/DasvKulXZzhSNFwx4cLRXKkSM9GPK7Y0nZ4FEylM=', + 'type': 'AC26791624DE60'}, + 'voting_power': 9}, + {'pub_key': {'data': 'GIijU7GBcVyiVUcB0GwWZbxCxdk2xV6pxdvL24s/AqM=', + 'type': 'AC26791624DE60'}, + 'voting_power': 7}, + {'pub_key': {'data': 'JbfwrLvCVIwOPm8tj8936ki7IYbmGHjPiKb6nAZegRA=', + 'type': 'AC26791624DE60'}, + 'voting_power': 10}, + {'pub_key': {'data': 'PecJ58SaNRsWJZodDmqjpCWqG6btdwXFHLyE40RYlYM=', + 'type': 'AC26791624DE60'}, + 'voting_power': 8}] + + b.get_validators = custom_mock_get_validators + status = ValidatorElection.INCONCLUSIVE + resp = inconclusive_election.get_status(b) + assert resp == status + + +def test_upsert_validator_show(caplog, ongoing_election, b): + from bigchaindb.commands.bigchaindb import run_upsert_validator_show + + election_id = ongoing_election.id + public_key = public_key_to_base64(ongoing_election.asset['data']['public_key']) + power = ongoing_election.asset['data']['power'] + node_id = ongoing_election.asset['data']['node_id'] + status = ValidatorElection.ONGOING + + show_args = Namespace(action='show', + election_id=election_id) + + msg = run_upsert_validator_show(show_args, b) + + assert msg == f'public_key={public_key}\npower={power}\nnode_id={node_id}\nstatus={status}'