1
0
mirror of https://github.com/bigchaindb/bigchaindb.git synced 2024-06-23 01:36:42 +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):
"""Data integrity error that requires attention"""
class CriticalDuplicateVote(BigchainDBError):
"""Data integrity error that requires attention"""

View File

@ -1,6 +1,7 @@
import collections
from bigchaindb.common.schema import SchemaValidationError, validate_vote_schema
from bigchaindb.exceptions import CriticalDuplicateVote
from bigchaindb.common.utils import serialize
from bigchaindb.common.crypto import PublicKey
@ -33,7 +34,8 @@ class Voting:
n_voters = len(eligible_voters)
eligible_votes, ineligible_votes = \
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['status'] = cls.decide_votes(n_voters, **results['counts'])
results['ineligible'] = ineligible_votes
@ -60,38 +62,29 @@ class Voting:
return eligible, ineligible
@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
as voters), produce the number that say valid and the number that say
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.
invalid. Votes must agree on previous block, otherwise they become invalid.
"""
prev_blocks = collections.Counter()
cheat = []
malformed = []
# Group by pubkey to detect duplicate voting
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]
for vote in by_voter.values():
if not cls.verify_vote_schema(vote):
malformed.append(vote)
continue
@ -111,7 +104,6 @@ class Voting:
'n_valid': n_valid,
'n_invalid': len(by_voter) - n_valid,
},
'cheat': cheat,
'malformed': malformed,
'previous_block': prev_block,
'other_previous_block': dict(prev_blocks),

View File

@ -2,6 +2,7 @@ import pytest
from collections import Counter
from bigchaindb.core import Bigchain
from bigchaindb.exceptions import CriticalDuplicateVote
from bigchaindb.voting import Voting, INVALID, VALID, UNDECIDED
@ -37,24 +38,22 @@ def test_count_votes():
def verify_vote_schema(cls, vote):
return vote['node_pubkey'] != 'malformed'
voters = (['cheat', 'cheat', 'says invalid', 'malformed'] +
voters = (['says invalid', 'malformed'] +
['kosher' + str(i) for i in range(10)])
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
votes[-1]['vote']['previous_block'] = 'z'
assert TestVoting.count_votes(votes) == {
by_voter = dict(enumerate(votes))
assert TestVoting.count_votes(by_voter) == {
'counts': {
'n_valid': 9, # 9 kosher votes
'n_invalid': 4, # 1 cheat, 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)
'n_invalid': 3, # 1 invalid, 1 malformed, 1 rogue prev block
},
'cheat': [votes[:2]],
'malformed': [votes[3]],
'malformed': [votes[1]],
'previous_block': 'a',
'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[0]['vote']['previous_block'] = 'b'
votes[1]['vote']['previous_block'] = 'c'
assert TestVoting.count_votes(votes) == {
by_voter = dict(enumerate(votes))
assert TestVoting.count_votes(by_voter) == {
'counts': {
'n_valid': 2,
'n_invalid': 2,
@ -78,7 +78,6 @@ def test_must_agree_prev_block():
'previous_block': 'a',
'other_previous_block': {'b': 1, 'c': 1},
'malformed': [],
'cheat': [],
}
@ -230,8 +229,22 @@ def test_block_election(b):
'block_id': 'xyz',
'counts': {'n_valid': 2, 'n_invalid': 0},
'ineligible': [votes[-1]],
'cheat': [],
'malformed': [],
'previous_block': 'a',
'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)