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:
parent
43f779a18b
commit
ddbdf64e33
|
@ -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"""
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in New Issue
Block a user