diff --git a/bigchaindb/backend/localmongodb/query.py b/bigchaindb/backend/localmongodb/query.py index c85e0854..29993b30 100644 --- a/bigchaindb/backend/localmongodb/query.py +++ b/bigchaindb/backend/localmongodb/query.py @@ -299,6 +299,18 @@ def get_validator_set(conn, height=None): return list(cursor)[0] +@register_query(LocalMongoDBConnection) +def get_validator_set_by_election_id(conn, election_id): + query = {'election_id': election_id} + + cursor = conn.run( + conn.collection('validators') + .find(query, projection={'_id': False}) + ) + + return next(cursor, None) + + @register_query(LocalMongoDBConnection) def get_asset_tokens_for_public_key(conn, asset_id, public_key): query = {'outputs.public_keys': [public_key], diff --git a/bigchaindb/backend/localmongodb/schema.py b/bigchaindb/backend/localmongodb/schema.py index 0607c883..1ad88928 100644 --- a/bigchaindb/backend/localmongodb/schema.py +++ b/bigchaindb/backend/localmongodb/schema.py @@ -133,3 +133,6 @@ def create_validators_secondary_index(conn, dbname): conn.conn[dbname]['validators'].create_index('height', name='height', unique=True,) + conn.conn[dbname]['validators'].create_index('election_id', + name='election_id', + unique=True,) diff --git a/bigchaindb/backend/query.py b/bigchaindb/backend/query.py index b2cef080..4d62c633 100644 --- a/bigchaindb/backend/query.py +++ b/bigchaindb/backend/query.py @@ -360,6 +360,14 @@ def get_validator_set(conn, height): raise NotImplementedError +@singledispatch +def get_validator_set_by_election_id(conn, election_id): + """Return a validator set change with the specified election_id + """ + + raise NotImplementedError + + @singledispatch def get_asset_tokens_for_public_key(connection, asset_id, public_key, operation): diff --git a/bigchaindb/core.py b/bigchaindb/core.py index 67b48df5..621960fa 100644 --- a/bigchaindb/core.py +++ b/bigchaindb/core.py @@ -54,7 +54,7 @@ class App(BaseApplication): 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) + self.bigchaindb.store_validator_set(1, validator_set, None) return ResponseInitChain() def info(self, request): diff --git a/bigchaindb/lib.py b/bigchaindb/lib.py index e75153d5..05ca3e69 100644 --- a/bigchaindb/lib.py +++ b/bigchaindb/lib.py @@ -421,24 +421,32 @@ class BigchainDB(object): def fastquery(self): return fastquery.FastQuery(self.connection) + def get_validator_change(self, height=None): + return backend.query.get_validator_set(self.connection, height) + def get_validators(self, height=None): - result = backend.query.get_validator_set(self.connection, height) + result = self.get_validator_change(height) validators = result['validators'] return validators + def get_validators_by_election_id(self, election_id): + result = backend.query.get_validator_set_by_election_id(self.connection, election_id) + return result + def delete_validator_update(self): return backend.query.delete_validator_update(self.connection) def store_pre_commit_state(self, state): return backend.query.store_pre_commit_state(self.connection, state) - def store_validator_set(self, height, validators): + def store_validator_set(self, height, validators, election_id): """Store validator set at a given `height`. NOTE: If the validator set already exists at that `height` then an exception will be raised. """ return backend.query.store_validator_set(self.connection, {'height': height, - 'validators': validators}) + 'validators': validators, + 'election_id': election_id}) Block = namedtuple('Block', ('app_hash', 'height', 'transactions')) diff --git a/bigchaindb/upsert_validator/validator_election.py b/bigchaindb/upsert_validator/validator_election.py index 89e60335..7d9a6fbc 100644 --- a/bigchaindb/upsert_validator/validator_election.py +++ b/bigchaindb/upsert_validator/validator_election.py @@ -5,7 +5,6 @@ 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, @@ -42,6 +41,21 @@ class ValidatorElection(Transaction): # of `CREATE` and any validation on `CREATE` in the parent class should apply to it super().__init__(operation, asset, inputs, outputs, metadata, version, hash_id) + @classmethod + def get_validator_change(cls, bigchain, height=None): + """Return the latest change to the validator set + + :return: { + 'height': , + 'asset': { + 'height': , + 'validators': , + 'election_id': + } + } + """ + 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 @@ -227,55 +241,24 @@ class ValidatorElection(Transaction): 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) + bigchain.store_validator_set(new_height+1, updated_validator_set, election.id) 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_validator_update_by_election_id(self, election_id, bigchain): + result = bigchain.get_validators_by_election_id(election_id) + return result - 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: + 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 diff --git a/tests/backend/localmongodb/test_queries.py b/tests/backend/localmongodb/test_queries.py index 51e20265..77bfddd0 100644 --- a/tests/backend/localmongodb/test_queries.py +++ b/tests/backend/localmongodb/test_queries.py @@ -380,7 +380,7 @@ def test_validator_update(): conn = connect() def gen_validator_update(height): - return {'data': 'somedata', 'height': height} + return {'data': 'somedata', 'height': height, 'election_id': f'election_id_at_height_{height}'} for i in range(1, 100, 10): value = gen_validator_update(i) diff --git a/tests/backend/localmongodb/test_schema.py b/tests/backend/localmongodb/test_schema.py index 136715ab..2481cd60 100644 --- a/tests/backend/localmongodb/test_schema.py +++ b/tests/backend/localmongodb/test_schema.py @@ -44,7 +44,7 @@ def test_init_creates_db_tables_and_indexes(): assert set(indexes) == {'_id_', 'pre_commit_id'} indexes = conn.conn[dbname]['validators'].index_information().keys() - assert set(indexes) == {'_id_', 'height'} + assert set(indexes) == {'_id_', 'height', 'election_id'} def test_init_database_fails_if_db_exists(): diff --git a/tests/conftest.py b/tests/conftest.py index 0fd4d671..7faa66c6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -631,6 +631,10 @@ def bad_validator_path(node_keys): @pytest.fixture def validators(b, node_keys): from bigchaindb.backend import query + import time + + def timestamp(): # we need this to force unique election_ids for setup and teardown of fixtures + return str(time.time()) height = get_block_height(b) @@ -645,7 +649,8 @@ def validators(b, node_keys): 'voting_power': 10}] validator_update = {'validators': validator_set, - 'height': height + 1} + 'height': height + 1, + 'election_id': f'setup_at_{timestamp()}'} query.store_validator_set(b.connection, validator_update) @@ -654,7 +659,8 @@ def validators(b, node_keys): height = get_block_height(b) validator_update = {'validators': original_validators, - 'height': height} + 'height': height, + 'election_id': f'teardown_at_{timestamp()}'} query.store_validator_set(b.connection, validator_update) diff --git a/tests/tendermint/test_core.py b/tests/tendermint/test_core.py index 6887264c..40958aa2 100644 --- a/tests/tendermint/test_core.py +++ b/tests/tendermint/test_core.py @@ -237,7 +237,7 @@ def test_new_validator_set(b): validators = [node1] updates = [node1_new_power, node2] - b.store_validator_set(1, validators) + b.store_validator_set(1, validators, 'election_id') updated_validator_set = new_validator_set(b.get_validators(1), updates) updated_validators = [] diff --git a/tests/upsert_validator/conftest.py b/tests/upsert_validator/conftest.py index 4a1b6925..20906ada 100644 --- a/tests/upsert_validator/conftest.py +++ b/tests/upsert_validator/conftest.py @@ -48,33 +48,39 @@ def valid_election_b(b, node_key, new_validator): @pytest.fixture def ongoing_election(b, valid_election, ed25519_node_keys): + validators = b.get_validators(height=1) + genesis_validators = {'validators': validators, + 'height': 0, + 'election_id': None} + query.store_validator_set(b.connection, genesis_validators) + 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()) + validators = b.get_validators(height=1) + validator_update = {'validators': validators, + 'height': 2, + 'election_id': ongoing_election.id} + + query.store_validator_set(b.connection, validator_update) return ongoing_election @pytest.fixture -def inconclusive_election(b, concluded_election, new_validator): +def inconclusive_election(b, ongoing_election, new_validator): validators = b.get_validators(height=1) validators[0]['voting_power'] = 15 validator_update = {'validators': validators, - 'height': 3} + 'height': 2, + 'election_id': 'some_other_election'} query.store_validator_set(b.connection, validator_update) - return concluded_election + return ongoing_election def vote(election, voter, keys, b): diff --git a/tests/upsert_validator/test_validator_election_vote.py b/tests/upsert_validator/test_validator_election_vote.py index 19eacbfa..6c92af99 100644 --- a/tests/upsert_validator/test_validator_election_vote.py +++ b/tests/upsert_validator/test_validator_election_vote.py @@ -234,7 +234,7 @@ def test_upsert_validator(b, node_key, node_keys, ed25519_node_keys): latest_block = b.get_latest_block() # reset the validator set - b.store_validator_set(latest_block['height'], validators) + b.store_validator_set(latest_block['height'], validators, 'previous_election_id') power = 1 public_key = '9B3119650DF82B9A5D8A12E38953EA47475C09F0C48A4E6A0ECE182944B24403' @@ -368,4 +368,4 @@ def reset_validator_set(b, node_keys, height): validators.append({'pub_key': {'type': 'ed25519', 'data': node_pub}, 'voting_power': 10}) - b.store_validator_set(height, validators) + b.store_validator_set(height, validators, 'election_id') diff --git a/tests/web/test_validators.py b/tests/web/test_validators.py index 714ceee4..52420828 100644 --- a/tests/web/test_validators.py +++ b/tests/web/test_validators.py @@ -14,7 +14,7 @@ def test_get_validators_endpoint(b, client): 'pub_key': {'data': '4E2685D9016126864733225BE00F005515200727FBAB1312FC78C8B76831255A', 'type': 'ed25519'}, 'voting_power': 10}] - b.store_validator_set(23, validator_set) + b.store_validator_set(23, validator_set, 'election_id') res = client.get(VALIDATORS_ENDPOINT) assert is_validator(res.json[0])