Skip to content

Create abstract election class#2498

Merged
ldmberman merged 28 commits intobigchaindb:masterfrom
z-bowen:abstract_election_class
Sep 11, 2018
Merged

Create abstract election class#2498
ldmberman merged 28 commits intobigchaindb:masterfrom
z-bowen:abstract_election_class

Conversation

@z-bowen
Copy link
Copy Markdown
Contributor

@z-bowen z-bowen commented Aug 31, 2018

**Problem: ValidatorElection and MigrationElection need to inherit from a generic Election class

Solution

Factored the common logic out of ValidatorElection and moved it to a common parent class, Election

…from a common `Election` class

Solution: Factored the common logic out of `ValidatorElection` and moved it to `Election` parent class
@z-bowen z-bowen requested review from kansi and ldmberman August 31, 2018 11:59
@codecov-io
Copy link
Copy Markdown

codecov-io commented Aug 31, 2018

Codecov Report

Merging #2498 into master will decrease coverage by 0.15%.
The diff coverage is 96.66%.

@@            Coverage Diff             @@
##           master    #2498      +/-   ##
==========================================
- Coverage   93.53%   93.37%   -0.16%     
==========================================
  Files          42       43       +1     
  Lines        2551     2582      +31     
==========================================
+ Hits         2386     2411      +25     
- Misses        165      171       +6


@register_query(LocalMongoDBConnection)
def get_validator_set_by_election_id(conn, election_id):
def get_result_by_election_id(conn, election_id, table):
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. A more appropriate function name could be get_election_result/get_election_result_by_id ?
  2. What is the point of this generalization?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Good suggestion. Changed the name.

  2. When checking the election status, the first thing I want to do is look and see if anything with this election_id has been written to the relevant Mongo table, (ie has the vote been previoously concluded.)


@register_query(LocalMongoDBConnection)
def get_validator_set_by_election_id(conn, election_id):
def get_result_by_election_id(conn, election_id, table):
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Creating a new collection for every election might be cumbersome. Instead, we can introduce an elections collection storing documents of the form:

{
    'election_id': '...',
    'type': '...',
    'height': X
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What would be the use of 'type'?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

type is what is operation in the transaction schema. For instance, if you want to find out if there was a validator update caused by the given election ID, you need to filter by type.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think you need type once you have the election id, there shouldn't be two elections with same election id and yet have different operations

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, but when you receive an election ID in the command line, you do not know its type for sure. A user may look for a validator election status providing a migration election ID by mistake.

'election_id': <election_id_that_approved_the_change>
}
}
"""
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this doc string is wrong?


        :return: {
                'height': <block_height>,
                'validators': <validator_set>,
                'election_id': <election_id_that_approved_the_change>
        }

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope. I checked it. It's correct.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't think so, bigchain.get_validator_change(height) calls backend.query.get_validator_set(self.connection, height) which returns the validator object of the following format,

{'height': <block_height>,
 'validators': <validator_set>,
 'election_id': <election_id_that_approved_the_change>
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

def count_votes(cls, election_pk, transactions, getter=getattr):
votes = 0
for txn in transactions:
if getter(txn, 'operation') == 'VALIDATOR_ELECTION_VOTE':
Copy link
Copy Markdown
Contributor

@kansi kansi Sep 3, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The generalized class cannot contain hard-coded value.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But 'VALIDATOR_ELECTION_VOTE' still exists

return result

@classmethod
def is_approved(cls, bigchain, new_height, txns):
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is_approved as a name suggests the function returns a boolean, while in fact it returns either [] or an arbitrary on_approve result.

I would add a function called get_approved_election_or_none, which returns what its name says. Additionally, in the ValidatorElection class I would keep get_validators_update, which in turn would use the generic get_approved_election_or_none.

What do you think?

Solution: Refactored `get_result_by_election_id` to `get_election_result_by_id`
@singledispatch
def get_asset_tokens_for_public_key(connection, asset_id,
public_key, operation):
def get_asset_tokens_for_public_key(connection, asset_id, public_key):
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

…ables

Solution: Remove `DB_TABLE` property from `Election` class
ldmberman added a commit to ldmberman/bigchaindb that referenced this pull request Sep 3, 2018
Solution: Election ID has to be unique but not every validator set record has it. MongoDB does not support partial indexes, does not even allow for multiple Nones. This is a temporary fix since we are introducing an `election` collection to store election IDs in bigchaindb#2498.
ldmberman added a commit that referenced this pull request Sep 3, 2018
Solution: Election ID has to be unique but not every validator set record has it. MongoDB does not support partial indexes, does not even allow for multiple Nones. This is a temporary fix since we are introducing an `election` collection to store election IDs in #2498.

# 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,)
ELECTION_TYPE = None
Copy link
Copy Markdown
Contributor

@kansi kansi Sep 3, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In an attempt to make the code more uniform it would good to rename ELECTION_TYPE to OPERATION so that further in future when CREATE and TRANSFER both become an independent class they would also have OPERATION field and the registeration would look as follows,

Transaction.register_type(Create.OPERATION, Create)
Transaction.register_type(Transfer.OPEATION, Transfer)
Transaction.register_type(ValidatorElection.OPERATION, ValidatorElection)
Transaction.register_type(ValidatorElectionVote.OPERATION, ValidatorElectionVote)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

… for an election method

Solution: Finished the refactoring
Solution: Created the `elections` table with secondary_index `election_id`
…tions in the `elections` table

Solution: Updated the class to use the new table
Solution: Renamed, refactored and moved the `Vote` class to tie in with the more general `Election` base class
Solution: Fixed the docstring
Solution: Pointed the reference to the class variable
…ote`

Solution:  Renamed `TX_SCHEMA_VALIDATOR_ELECTION_VOTE` as `TX_SCHEMA_VOTE`
Solution: Renamed `ELECTION_TYPE` to `OPERATION`
operation:
type: string
value: "VALIDATOR_ELECTION_VOTE"
value: "OPERATION"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Value is supposed to be VOTE

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is weird that the CI tests are still passing even when the election created using .generate would not match the above schema!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but the value is still "OPERATION"?

TRANSFER = VALIDATOR_ELECTION_VOTE
ALLOWED_OPERATIONS = (VALIDATOR_ELECTION_VOTE,)
TRANSFER = VOTE
ALLOWED_OPERATIONS = (VOTE,)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OPERATION = VOTE should also be specified.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see OPERATION = VOTE specified anywhere

bigchain.store_election(height, election)

@classmethod
def is_approved(cls, bigchain, new_height, txns):
Copy link
Copy Markdown
Contributor

@kansi kansi Sep 5, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is_approved as the name suggests should return True or False but it is returning [] or the respective election's proposed change.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

renamed


self.store_abci_chain(block['height'] + 1, new_chain_id, False)

def store_election(self, height, election):
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The doc string says Store election results but the function name says store_election. It would be better to name the function too as store_election_results

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still see the function name as store_election

Solution: Addressed comments from PR
:param current_transactions: (list) A list of transactions to be validated along with the election

Returns:
ValidatorElection object
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Return value should also have a generalized name

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

assert resp.validator_updates[0] == encode_validator(validator)

updates = b.get_validator_update()
updates = b.is_approved()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no is_approved I think. It seems to be changed to on_approved

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

approved_updates is the replacement method for is_approved. This test is skipped though. Are we planning on fixing it?

query.store_validator_update(b.connection, validator_update)

updates = b.get_validator_update()
updates = b.is_approved()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no is_approved. It seems to be changed to on_approved

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here. Are we going to un-skip this test?

Solution: Removed the variable and hardcoded everything to use the `Vote` class
Solution: Addressing comments
else:
return self.ONGOING
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)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As we decided in our discussion we will not store election.id in the validators collection instead store the fact that election.id concluded in the elections collections

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

input_conditions = []

duplicates = any(txn for txn in current_transactions if txn.id == self.id)
if bigchain.get_transaction(self.id) or duplicates:
Copy link
Copy Markdown
Contributor

@kansi kansi Sep 7, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be better to use bigchain.is_committed instead of bigchain.get_transaction

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see anything by that name in bigchaindb/lib.py, and it doesn't work when I try to execute bigchain.get_commited in the tests.

If we want to create this method can we do it in another PR? It doesn't seem necessary to get Election working

Copy link
Copy Markdown
Contributor

@kansi kansi Sep 10, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I was talking about this. Fixed the comment. Also, the reason I am suggesting this change is that the get_transaction required 3 database calls while is_committed requires only 1.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

…a `validator_set`

Solution: Removed the code to store the `election_id` and aligned the tests
…roperties

Solution: Added a random `uuid4` seed to enforce uniqueness
update = ValidatorElection.approved_update(b, 4, [tx_vote2])
if update:
update_public_key = codecs.encode(update.pub_key.data, 'base64').decode().rstrip('\n')
assert update
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems that we expect the update to have the new validator set. Since this is a test we don't need to play safe by using if update, we could do the following too,

update = ValidatorElection.approved_update(b, 4, [tx_vote2])
assert update
update_public_key = codecs.encode(update.pub_key.data, 'base64').decode().rstrip('\n')

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

update_public_key = None
if update:
update_public_key = codecs.encode(update.pub_key.data, 'base64').decode().rstrip('\n')
assert update
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could do the following and avoid if and update_public_key = None

update = ValidatorElection.approved_update(b, 4, [tx_vote0, tx_vote1, tx_vote2]) 
assert update 
update_public_key = codecs.encode(update.pub_key.data, 'base64').decode().rstrip('\n') 

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

name='height',
unique=True,)

logger.info('Create `abci_chains.chain_id` secondary index.')
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a conflict resolution leftover.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Restored



@singledispatch
def store_election_results(conn, validator_update):
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The parameter was not renamed.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

@@ -63,7 +63,7 @@ definitions:
- CREATE
- TRANSFER
- VALIDATOR_ELECTION
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We also need a generic name instead of VALIDATOR_ELECTION now.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What purpose would that serve since the name would always be overriden by the specific election type?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True, there is no purpose in such an operation..

But extending the base schema every time a new election is introduced does not sound necessary either.

I would not require the operation in the base spec. Every specific spec can require the particular operation itself.

Does not have to be done in this PR though.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we do...

Since Election is a base class, and will be extended with distinct election classes, shouldn't each of those get their own operations? The ValidatorElection is still using VALIDATOR_ELECTION as it's operation.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. An extra point (we can address it later) is that bigchaindb/common/schema/transaction_v2.0.yaml does not have to require the operation to be from the given set. It just makes us extend this list every time a new operation is introduced.

- node_id
- public_key
- power
- seed
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't have to make it required.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should it be required for the validator election?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do not have to require it there either.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's generated automatically on creation of the election, so it will always be present, regardless. But I can remove the required tag, anyway.

Copy link
Copy Markdown
Contributor

@kansi kansi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work 🎆

:return: {
'height': <block_height>,
'validators': <validator_set>,
'election_id': <election_id_that_approved_the_change>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

election_id is not there anymore.

(inputs, outputs) = cls.validate_transfer(inputs, recipients, election_id, metadata)
election_vote = cls(cls.VALIDATOR_ELECTION_VOTE, {'id': election_id}, inputs, outputs, metadata)
election_vote = cls(cls.OPERATION, {'id': election_id}, inputs, outputs, metadata)
cls.validate_schema(election_vote.to_dict(), skip_id=True)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can also remove skip_id from here.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why?

Copy link
Copy Markdown
Contributor

@ldmberman ldmberman Sep 11, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because Vote is always a TRANSFER, and we always validate how identifiers are built for transfers.

This is not true actually..

Discussed offline - we can remove skip_id but then we also need update from_dict in common/transaction.py so that it does not pass skip_id but calls validate_id after validating the schema.

return self

@classmethod
def generate(cls, initiator, voters, election_data, metadata=None):
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think about adding an additional parameter generate_random_seed=True? - it will simplify those tests that currently have to mock uuid4 and will clearly display to the reader that generating a seed is optional.

@ldmberman ldmberman merged commit 0fe749d into bigchaindb:master Sep 11, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants