Problem: (BEP-21) no way to propose validator election#2392
Problem: (BEP-21) no way to propose validator election#2392ldmberman merged 10 commits intobigchaindb:masterfrom
Conversation
Solution: Integrate the new election spec specifed in BEP-21
Solution: Handle operation to class for invalid transactions
Codecov Report
@@ 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): |
There was a problem hiding this comment.
Looks like the unspent_outputs, inputs_valid, and get_asset_id methods are broken now.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Ideally, we should have a base transaction class without
createandtransfer.
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) |
There was a problem hiding this comment.
Does it make sense to update the base from_dict instead?
There was a problem hiding this comment.
I think we can improve this by keeping the first two validations and then calling super().from_dict()?
There was a problem hiding this comment.
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()) |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
Returning False is inconsistent with raising exceptions and is not documented.
bigchaindb/tendermint/lib.py
Outdated
| 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]) |
There was a problem hiding this comment.
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): |
There was a problem hiding this comment.
Looks like these two with pytest.raises(DuplicateTransaction): are following the same code path.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
| return self.is_same_topology(current_validators, self.outputs) | ||
|
|
||
| @classmethod | ||
| def generate(cls, initiator, voters, election_data, metadata=None): |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
It would be helpful to also consider elections which add up more than 1/3 of the current total validator power to be invalid.
330a9a2 to
930d76b
Compare
|
|
||
| # NOTE: Proposer should be a single node | ||
| if len(self.inputs) != 1: | ||
| raise TypeError('`tx_signers` must be a list instance or length one') |
There was a problem hiding this comment.
It makes sense to have a specifc exception in the case like this so that a caller can easily distinguish exceptions.
There was a problem hiding this comment.
You mean a custom exception?
| 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()): |
Solution: Election which allocate power more 1/3 of current total power should be considered invalid
4113407 to
6a00c08
Compare
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
bigchaindb/lib.py
Outdated
| self.connection, *unspent_outputs) | ||
|
|
||
| def get_transaction(self, transaction_id, include_status=False): | ||
| def get_transaction(self, transaction_id, include_status=False, cls=Transaction): |
There was a problem hiding this comment.
No need to used the additional cls i.e. .from_dict implicitly chooses the correct class.
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') |
There was a problem hiding this comment.
Can you add a test case for an invalid proposer?
| Args: | ||
| bigchain (BigchainDB): an instantiated bigchaindb.tendermint.BigchainDB object. | ||
|
|
||
| Returns: |
There was a problem hiding this comment.
The "Returns" section is outdated.
| alloacted according to the voting power of each validator node. | ||
|
|
||
| Args: | ||
| bigchain (BigchainDB): an instantiated bigchaindb.tendermint.BigchainDB object. |
| def validate(self, bigchain, current_transactions=[]): | ||
| """Validate election transaction | ||
| NOTE: | ||
| * A valid election is initiated by an existing validator. |
There was a problem hiding this comment.
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.
|
Removing @vrde from review as he is on holiday. |
| """Raised if the public key is not a part of the validator set""" | ||
|
|
||
|
|
||
| class UnequalValidatorSet(ValidationError): |
There was a problem hiding this comment.
nitpick: InvalidValidatorSet makes more sense for this exception?
There was a problem hiding this comment.
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.
muawiakh
left a comment
There was a problem hiding this comment.
lgtm, would be good to have an acceptance test later on when this feature is merged.
Solution: Integrate the new election spec specified in BEP-21
Implements one of the tasks in issue #2372