Skip to content

Problem: (BEP-21) no way to propose validator election#2392

Merged
ldmberman merged 10 commits intobigchaindb:masterfrom
kansi:21/validator_election
Jul 27, 2018
Merged

Problem: (BEP-21) no way to propose validator election#2392
ldmberman merged 10 commits intobigchaindb:masterfrom
kansi:21/validator_election

Conversation

@kansi
Copy link
Copy Markdown
Contributor

@kansi kansi commented Jul 18, 2018

Solution: Integrate the new election spec specified in BEP-21

Implements one of the tasks in issue #2372

Solution: Integrate the new election spec specifed in BEP-21
@kansi kansi requested review from ldmberman and vrde July 18, 2018 13:28
@kansi kansi mentioned this pull request Jul 18, 2018
14 tasks
Solution: Handle operation to class for invalid transactions
@codecov-io
Copy link
Copy Markdown

codecov-io commented Jul 18, 2018

Codecov Report

Merging #2392 into master will increase coverage by 0.36%.
The diff coverage is 96.38%.

@@            Coverage Diff             @@
##           master    #2392      +/-   ##
==========================================
+ Coverage    86.9%   87.26%   +0.36%     
==========================================
  Files          38       39       +1     
  Lines        2168     2270     +102     
==========================================
+ Hits         1884     1981      +97     
- Misses        284      289       +5

TX_SCHEMA_VERSION, __file__)


class ValidatorElection(Transaction):
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.

Looks like the unspent_outputs, inputs_valid, and get_asset_id methods are broken now.

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.

Why?

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.

These functions expect self.operation to be one of Transaction.CREATE, Transaction.TRANSFER, what is not the case for ValidatorElection.

class ValidatorElection(Transaction):

VALIDATOR_ELECTION = 'VALIDATOR_ELECTION'
# NOTE: this transaction class extends create so the operation inheritence is achieved
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.

May be it is cleaner to override cls.create to raise an exception, what do you think? Or do you want to use ValidatorElection for creating election votes later?

Copy link
Copy Markdown
Contributor Author

@kansi kansi Jul 24, 2018

Choose a reason for hiding this comment

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

May be it is cleaner to override cls.create to raise an exception, what do you think?

Cloud you please elaborate? What would be achieved if we override cls.create and raise an exception 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.

Currently, there are three methods for instantiating an election transaction - ValidatorElection.create, ValidatorElection.generate, and the constructor. It is easier to read and use the code if we only keep one. The problem with create though is that it comes from the parent so raising an exception is a way to explain that this method is not for use. Ideally, we should have a base transaction class without create and transfer.

Copy link
Copy Markdown
Contributor Author

@kansi kansi Jul 24, 2018

Choose a reason for hiding this comment

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

Ideally, we should have a base transaction class without create and transfer.

That is the target. In this PR I am trying to consolidate the methods which are required in the base Transaction class so other new transaction classes can be written by extending it.


@classmethod
def from_dict(cls, tx):
cls.validate_id(tx)
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.

Does it make sense to update the base from_dict instead?

Copy link
Copy Markdown
Contributor Author

@kansi kansi Jul 24, 2018

Choose a reason for hiding this comment

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

I think we can improve this by keeping the first two validations and then calling super().from_dict()?

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 why don't you add these two to the base class? Why do we want to validate this part only for election transactions?


# Check whether the voters and their votes is same to that of the
# validators and their voting power in the network
return (current_topology.items() == voters.items())
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.

Although the new Python guarantees the key order, I would compare dicts directly current_topology == voters, for simplicity.


# NOTE: Proposer should be a single node
if len(self.inputs) != 1:
return False
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.

Returning False is inconsistent with raising exceptions and is not documented.

raise DoubleSpend('tx "{}" spends inputs twice'.format(txid))
elif transactions:
transaction = Transaction.from_db(self, transactions[0])
transaction = operation_class(transactions[0]).from_db(self, transactions[0])
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.

To make the core transaction module independent from the plugin modules, we can do DI the following way:

class Transaction:
    type_registry = {}

    @classmethod
    def register_type(typ, clas):
        type_registry[typ] = clas

    @classmethod
    def from_whatever(tx):
        return cls(type_registry[tx['operation']])

Then, inside a_plugin.py one can do smth like:

from core import Transaction

Transaction.register_type('election', ValidatorElection)

What do you think?


b_mock.store_bulk_transactions([valid_election])

with pytest.raises(DuplicateTransaction):
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.

Looks like these two with pytest.raises(DuplicateTransaction): are following the same code path.

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.

Not quite, the previous exception will be generated because valid_election is a part of current_transactions list and the second exception would be generated because valid_election has been stored in MongoDB and should be read from there to raise the exception.

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, you just need to call valid_election.validate(b_mock) in the second case. Otherwise you depend on the order of the checks.

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.

oh yeah, my bad.

return self.is_same_topology(current_validators, self.outputs)

@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.

Is there a particular reason for having both the constructor and generate?

return False

# NOTE: Check if all validators have been assigned votes equal to their voting power
return self.is_same_topology(current_validators, self.outputs)
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 would be helpful to also consider elections which add up more than 1/3 of the current total validator power to be invalid.

@kansi kansi force-pushed the 21/validator_election branch from 330a9a2 to 930d76b Compare July 24, 2018 12:38

# NOTE: Proposer should be a single node
if len(self.inputs) != 1:
raise TypeError('`tx_signers` must be a list instance or length one')
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 makes sense to have a specifc exception in the case like this so that a caller can easily distinguish exceptions.

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.

You mean a custom exception?

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.

raise TypeError('`tx_signers` must be a list instance or length one')

# NOTE: change more than 1/3 of the current power is not allowed
if self.asset['data']['power'] > (1/3)*sum(current_validators.values()):
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.

Isn't it >= 1/3?

Solution: Election which allocate power more 1/3 of current total power should
be considered invalid
@kansi kansi force-pushed the 21/validator_election branch 2 times, most recently from 4113407 to 6a00c08 Compare July 24, 2018 12:49
kansi added 4 commits July 25, 2018 14:51
Solution: Create registry for store transaction types and their corresponding classes
Solution: Import Transaction form models.py module
Solution: Override methods and raise execptions for code clarity
self.connection, *unspent_outputs)

def get_transaction(self, transaction_id, include_status=False):
def get_transaction(self, transaction_id, include_status=False, cls=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.

No need to used the additional cls i.e. .from_dict implicitly chooses the correct class.

kansi added 2 commits July 25, 2018 17:47
Solution: the `.from_dict` method implicitly detects the class so `cls`
parameter can be removed
Solution: All errors should generate exceptions extended from ValidationError class
# 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')
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.

Can you add a test case for an invalid proposer?

Args:
bigchain (BigchainDB): an instantiated bigchaindb.tendermint.BigchainDB object.

Returns:
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 "Returns" section is outdated.

alloacted according to the voting power of each validator node.

Args:
bigchain (BigchainDB): an instantiated bigchaindb.tendermint.BigchainDB 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.

The location of BigchainDB has been changed.

def validate(self, bigchain, current_transactions=[]):
"""Validate election transaction
NOTE:
* A valid election is initiated by an existing validator.
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 putting a reference to the BEP here? Then, a reader will get the complete context and we won't have to maintain the validation documentation in two places.

Solution: Add tests which raise InvalidPropser exception if the public key not a
part of the validator set and raises MultipleInputsError if more than 1
validators `owners_before` in the election.
@kansi
Copy link
Copy Markdown
Contributor Author

kansi commented Jul 27, 2018

Removing @vrde from review as he is on holiday.

@kansi kansi requested review from muawiakh and removed request for vrde July 27, 2018 08:57
"""Raised if the public key is not a part of the validator set"""


class UnequalValidatorSet(ValidationError):
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.

nitpick: InvalidValidatorSet makes more sense for this exception?

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 thought about that name InvalidValidatorSet, but the code here tries to detect if the validator sets are the same or different. The only thing we wish to imply is their inequality and not validity.

Copy link
Copy Markdown
Contributor

@muawiakh muawiakh left a comment

Choose a reason for hiding this comment

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

lgtm, would be good to have an acceptance test later on when this feature is merged.

@ldmberman ldmberman merged commit 7dcdefc into bigchaindb:master Jul 27, 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