1
0
mirror of https://github.com/bigchaindb/bigchaindb.git synced 2024-06-28 00:27:45 +02:00

voting module raises CriticalDuplicateVote if there's a duplicate vote

This commit is contained in:
Scott Sadler 2017-03-20 17:30:02 +01:00
parent 43f779a18b
commit ddbdf64e33
3 changed files with 48 additions and 39 deletions

View File

@ -8,3 +8,7 @@ class CriticalDoubleSpend(BigchainDBError):
class CriticalDoubleInclusion(BigchainDBError): class CriticalDoubleInclusion(BigchainDBError):
"""Data integrity error that requires attention""" """Data integrity error that requires attention"""
class CriticalDuplicateVote(BigchainDBError):
"""Data integrity error that requires attention"""

View File

@ -1,6 +1,7 @@
import collections import collections
from bigchaindb.common.schema import SchemaValidationError, validate_vote_schema from bigchaindb.common.schema import SchemaValidationError, validate_vote_schema
from bigchaindb.exceptions import CriticalDuplicateVote
from bigchaindb.common.utils import serialize from bigchaindb.common.utils import serialize
from bigchaindb.common.crypto import PublicKey from bigchaindb.common.crypto import PublicKey
@ -33,7 +34,8 @@ class Voting:
n_voters = len(eligible_voters) n_voters = len(eligible_voters)
eligible_votes, ineligible_votes = \ eligible_votes, ineligible_votes = \
cls.partition_eligible_votes(votes, eligible_voters) cls.partition_eligible_votes(votes, eligible_voters)
results = cls.count_votes(eligible_votes) by_voter = cls.dedupe_by_voter(eligible_votes)
results = cls.count_votes(by_voter)
results['block_id'] = block['id'] results['block_id'] = block['id']
results['status'] = cls.decide_votes(n_voters, **results['counts']) results['status'] = cls.decide_votes(n_voters, **results['counts'])
results['ineligible'] = ineligible_votes results['ineligible'] = ineligible_votes
@ -60,38 +62,29 @@ class Voting:
return eligible, ineligible return eligible, ineligible
@classmethod @classmethod
def count_votes(cls, eligible_votes): def dedupe_by_voter(cls, eligible_votes):
"""
Throw a critical error if there is a duplicate vote
"""
by_voter = {}
for vote in eligible_votes:
pubkey = vote['node_pubkey']
if pubkey in by_voter:
raise CriticalDuplicateVote(pubkey)
by_voter[pubkey] = vote
return by_voter
@classmethod
def count_votes(cls, by_voter):
""" """
Given a list of eligible votes, (votes from known nodes that are listed Given a list of eligible votes, (votes from known nodes that are listed
as voters), produce the number that say valid and the number that say as voters), produce the number that say valid and the number that say
invalid. invalid. Votes must agree on previous block, otherwise they become invalid.
* Detect if there are multiple votes from a single node and return them
in a separate "cheat" dictionary.
* Votes must agree on previous block, otherwise they become invalid.
note:
The sum of votes returned by this function does not necessarily
equal the length of the list of votes fed in. It may differ for
example if there are found to be multiple votes submitted by a
single voter.
""" """
prev_blocks = collections.Counter() prev_blocks = collections.Counter()
cheat = []
malformed = [] malformed = []
# Group by pubkey to detect duplicate voting for vote in by_voter.values():
by_voter = collections.defaultdict(list)
for vote in eligible_votes:
by_voter[vote['node_pubkey']].append(vote)
for pubkey, votes in by_voter.items():
if len(votes) > 1:
cheat.append(votes)
continue
vote = votes[0]
if not cls.verify_vote_schema(vote): if not cls.verify_vote_schema(vote):
malformed.append(vote) malformed.append(vote)
continue continue
@ -111,7 +104,6 @@ class Voting:
'n_valid': n_valid, 'n_valid': n_valid,
'n_invalid': len(by_voter) - n_valid, 'n_invalid': len(by_voter) - n_valid,
}, },
'cheat': cheat,
'malformed': malformed, 'malformed': malformed,
'previous_block': prev_block, 'previous_block': prev_block,
'other_previous_block': dict(prev_blocks), 'other_previous_block': dict(prev_blocks),

View File

@ -2,6 +2,7 @@ import pytest
from collections import Counter from collections import Counter
from bigchaindb.core import Bigchain from bigchaindb.core import Bigchain
from bigchaindb.exceptions import CriticalDuplicateVote
from bigchaindb.voting import Voting, INVALID, VALID, UNDECIDED from bigchaindb.voting import Voting, INVALID, VALID, UNDECIDED
@ -37,24 +38,22 @@ def test_count_votes():
def verify_vote_schema(cls, vote): def verify_vote_schema(cls, vote):
return vote['node_pubkey'] != 'malformed' return vote['node_pubkey'] != 'malformed'
voters = (['cheat', 'cheat', 'says invalid', 'malformed'] + voters = (['says invalid', 'malformed'] +
['kosher' + str(i) for i in range(10)]) ['kosher' + str(i) for i in range(10)])
votes = [Bigchain(v).vote('block', 'a', True) for v in voters] votes = [Bigchain(v).vote('block', 'a', True) for v in voters]
votes[2]['vote']['is_block_valid'] = False votes[0]['vote']['is_block_valid'] = False
# Incorrect previous block subtracts from n_valid and adds to n_invalid # Incorrect previous block subtracts from n_valid and adds to n_invalid
votes[-1]['vote']['previous_block'] = 'z' votes[-1]['vote']['previous_block'] = 'z'
assert TestVoting.count_votes(votes) == { by_voter = dict(enumerate(votes))
assert TestVoting.count_votes(by_voter) == {
'counts': { 'counts': {
'n_valid': 9, # 9 kosher votes 'n_valid': 9, # 9 kosher votes
'n_invalid': 4, # 1 cheat, 1 invalid, 1 malformed, 1 rogue prev block 'n_invalid': 3, # 1 invalid, 1 malformed, 1 rogue prev block
# One of the cheat votes counts towards n_invalid, the other is
# not counted here.
# len(cheat) + n_valid + n_invalid == len(votes)
}, },
'cheat': [votes[:2]], 'malformed': [votes[1]],
'malformed': [votes[3]],
'previous_block': 'a', 'previous_block': 'a',
'other_previous_block': {'z': 1}, 'other_previous_block': {'z': 1},
} }
@ -70,7 +69,8 @@ def test_must_agree_prev_block():
votes = [Bigchain(v).vote('block', 'a', True) for v in voters] votes = [Bigchain(v).vote('block', 'a', True) for v in voters]
votes[0]['vote']['previous_block'] = 'b' votes[0]['vote']['previous_block'] = 'b'
votes[1]['vote']['previous_block'] = 'c' votes[1]['vote']['previous_block'] = 'c'
assert TestVoting.count_votes(votes) == { by_voter = dict(enumerate(votes))
assert TestVoting.count_votes(by_voter) == {
'counts': { 'counts': {
'n_valid': 2, 'n_valid': 2,
'n_invalid': 2, 'n_invalid': 2,
@ -78,7 +78,6 @@ def test_must_agree_prev_block():
'previous_block': 'a', 'previous_block': 'a',
'other_previous_block': {'b': 1, 'c': 1}, 'other_previous_block': {'b': 1, 'c': 1},
'malformed': [], 'malformed': [],
'cheat': [],
} }
@ -230,8 +229,22 @@ def test_block_election(b):
'block_id': 'xyz', 'block_id': 'xyz',
'counts': {'n_valid': 2, 'n_invalid': 0}, 'counts': {'n_valid': 2, 'n_invalid': 0},
'ineligible': [votes[-1]], 'ineligible': [votes[-1]],
'cheat': [],
'malformed': [], 'malformed': [],
'previous_block': 'a', 'previous_block': 'a',
'other_previous_block': {}, 'other_previous_block': {},
} }
def test_duplicate_vote_throws_critical_error(b):
class TestVoting(Voting):
@classmethod
def verify_vote_signature(cls, vote):
return True
keyring = 'abc'
block = {'id': 'xyz', 'block': {'voters': 'ab'}}
votes = [{
'node_pubkey': c,
'vote': {'is_block_valid': True, 'previous_block': 'a'}
} for c in 'aabc']
with pytest.raises(CriticalDuplicateVote):
TestVoting.block_election(block, votes, keyring)