mirror of
https://github.com/bigchaindb/bigchaindb.git
synced 2024-10-13 13:34:05 +00:00
flesh out voting module
This commit is contained in:
parent
dbfcce34e7
commit
d71e560ba4
@ -1,25 +1,41 @@
|
|||||||
|
"""
|
||||||
|
Everything to do with creating and checking votes.
|
||||||
|
All functions in this module should be referentially transparent, that is,
|
||||||
|
they always give the same output for a given input. This makes it easier
|
||||||
|
to test.
|
||||||
|
"""
|
||||||
import collections
|
import collections
|
||||||
|
|
||||||
|
|
||||||
def filter_eligible_votes(votes, block_voters, keyring, check_signature):
|
def partition_eligible_votes(votes, eligible_voters, verify_vote_signature):
|
||||||
"""
|
"""
|
||||||
Filter votes from unknown nodes or nodes that are not listed on
|
Filter votes from unknown nodes or nodes that are not listed on
|
||||||
block. Here is our sybill protection.
|
block. This is the primary Sybill protection.
|
||||||
"""
|
"""
|
||||||
eligible_voters = set(keyring) & set(block_voters)
|
eligible, ineligible = ([], [])
|
||||||
eligible_votes = []
|
|
||||||
|
|
||||||
for vote in votes:
|
for vote in votes:
|
||||||
pubkey = vote['node_pubkey']
|
voter_eligible = vote['node_pubkey'] in eligible_voters
|
||||||
voter_eligible = pubkey in eligible_voters
|
if voter_eligible and verify_vote_signature(vote):
|
||||||
sig_legit = sig_is_legit(vote)
|
eligible.append(vote)
|
||||||
if voter_eligible and sig_legit:
|
else:
|
||||||
eligible_votes[pubkey].append(vote)
|
ineligible.append(vote)
|
||||||
|
|
||||||
return eligible_votes
|
return eligible, ineligible
|
||||||
|
|
||||||
|
|
||||||
def count_votes(eligible_votes, check_schema):
|
def count_votes(eligible_votes):
|
||||||
|
"""
|
||||||
|
Given a list of eligible votes, (votes from known nodes that are listed
|
||||||
|
as voters), count the votes to produce three quantities:
|
||||||
|
|
||||||
|
Number of votes that say valid
|
||||||
|
Number of votes that say invalid
|
||||||
|
Highest agreement on previous block ID
|
||||||
|
|
||||||
|
Also, detect if there are multiple votes from a single node and return them
|
||||||
|
in a separate "cheat" dictionary.
|
||||||
|
"""
|
||||||
by_voter = collections.defaultdict(list)
|
by_voter = collections.defaultdict(list)
|
||||||
for vote in eligible_votes:
|
for vote in eligible_votes:
|
||||||
by_voter[vote['node_pubkey']].append(vote)
|
by_voter[vote['node_pubkey']].append(vote)
|
||||||
@ -27,9 +43,11 @@ def count_votes(eligible_votes, check_schema):
|
|||||||
n_valid = 0
|
n_valid = 0
|
||||||
n_invalid = 0
|
n_invalid = 0
|
||||||
prev_blocks = collections.Counter()
|
prev_blocks = collections.Counter()
|
||||||
|
cheat = {}
|
||||||
|
|
||||||
for pubkey, votes in by_voter.items():
|
for pubkey, votes in by_voter.items():
|
||||||
if len(votes) > 1 or not schema_is_correct(votes[0]):
|
if len(votes) > 1:
|
||||||
|
cheat[pubkey] = votes
|
||||||
n_invalid += 1
|
n_invalid += 1
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@ -41,7 +59,41 @@ def count_votes(eligible_votes, check_schema):
|
|||||||
n_invalid += 1
|
n_invalid += 1
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'valid': n_valid,
|
'n_valid': n_valid,
|
||||||
'invalid': n_invalid,
|
'n_invalid': n_invalid,
|
||||||
'prev_block': prev_blocks.most_common()[0]
|
'n_agree_prev_block': prev_blocks.most_common()[0][1]
|
||||||
}
|
}, cheat
|
||||||
|
|
||||||
|
|
||||||
|
def decide_votes(n_voters, n_valid, n_invalid, n_agree_prev_block):
|
||||||
|
"""
|
||||||
|
Decide on votes.
|
||||||
|
|
||||||
|
To return VALID there must be a clear majority that say VALID
|
||||||
|
and also agree on the previous block. This is achieved using the > operator.
|
||||||
|
|
||||||
|
A tie on an even number of votes counts as INVALID so the >= operator is
|
||||||
|
used.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Check insane cases. This is basic, not exhaustive.
|
||||||
|
if n_valid + n_invalid > n_voters or n_agree_prev_block > n_voters:
|
||||||
|
raise ValueError('Arguments not sane: %s' % {
|
||||||
|
'n_voters': n_voters,
|
||||||
|
'n_valid': n_valid,
|
||||||
|
'n_invalid': n_invalid,
|
||||||
|
'n_agree_prev_block': n_agree_prev_block,
|
||||||
|
})
|
||||||
|
|
||||||
|
if n_invalid * 2 >= n_voters:
|
||||||
|
return INVALID
|
||||||
|
if n_valid * 2 > n_voters:
|
||||||
|
if n_agree_prev_block * 2 > n_voters:
|
||||||
|
return VALID
|
||||||
|
return INVALID
|
||||||
|
return UNDECIDED
|
||||||
|
|
||||||
|
|
||||||
|
INVALID = 'invalid'
|
||||||
|
VALID = TX_VALID = 'valid'
|
||||||
|
UNDECIDED = TX_UNDECIDED = 'undecided'
|
||||||
|
61
tests/test_voting.py
Normal file
61
tests/test_voting.py
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from bigchaindb.core import Bigchain
|
||||||
|
from bigchaindb.voting import (count_votes, partition_eligible_votes,
|
||||||
|
decide_votes, INVALID, VALID, UNDECIDED)
|
||||||
|
|
||||||
|
|
||||||
|
def test_partition_eligible_votes():
|
||||||
|
nodes = list(map(Bigchain, 'abc'))
|
||||||
|
votes = [n.vote('block', 'a', True) for n in nodes]
|
||||||
|
|
||||||
|
el, inel = partition_eligible_votes(votes, 'abc', lambda _: True)
|
||||||
|
|
||||||
|
assert el == votes
|
||||||
|
assert inel == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_count_votes():
|
||||||
|
nodes = list(map(Bigchain, 'abc'))
|
||||||
|
votes = [n.vote('block', 'a', True) for n in nodes]
|
||||||
|
|
||||||
|
assert count_votes(votes) == ({
|
||||||
|
'n_valid': 3,
|
||||||
|
'n_invalid': 0,
|
||||||
|
'n_agree_prev_block': 3
|
||||||
|
}, {})
|
||||||
|
|
||||||
|
|
||||||
|
DECISION_TESTS = [dict(
|
||||||
|
zip(['n_voters', 'n_valid', 'n_invalid', 'n_agree_prev_block'], t))
|
||||||
|
for t in [
|
||||||
|
(1, 1, 1, 1),
|
||||||
|
(2, 2, 1, 2),
|
||||||
|
(3, 2, 2, 2),
|
||||||
|
(4, 3, 2, 3),
|
||||||
|
(5, 3, 3, 3),
|
||||||
|
(6, 4, 3, 4),
|
||||||
|
(7, 4, 4, 4),
|
||||||
|
(8, 5, 4, 5),
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('kwargs', DECISION_TESTS)
|
||||||
|
def test_decide_votes_valid(kwargs):
|
||||||
|
kwargs = kwargs.copy()
|
||||||
|
kwargs['n_invalid'] = 0
|
||||||
|
assert decide_votes(**kwargs) == VALID
|
||||||
|
kwargs['n_agree_prev_block'] -= 1
|
||||||
|
assert decide_votes(**kwargs) == INVALID
|
||||||
|
kwargs['n_valid'] -= 1
|
||||||
|
assert decide_votes(**kwargs) == UNDECIDED
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('kwargs', DECISION_TESTS)
|
||||||
|
def test_decide_votes_invalid(kwargs):
|
||||||
|
kwargs = kwargs.copy()
|
||||||
|
kwargs['n_valid'] = 0
|
||||||
|
assert decide_votes(**kwargs) == INVALID
|
||||||
|
kwargs['n_invalid'] -= 1
|
||||||
|
assert decide_votes(**kwargs) == UNDECIDED
|
Loading…
x
Reference in New Issue
Block a user