Static Classify Voting

This commit is contained in:
Scott Sadler 2017-02-23 17:58:08 +01:00
parent 20f6539e10
commit fdad8cd796
2 changed files with 174 additions and 100 deletions

View File

@ -1,10 +1,7 @@
"""
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
from bigchaindb.common.schema import SchemaValidationError, validate_vote_schema
from bigchaindb.common.utils import serialize
from bigchaindb.common.crypto import PublicKey
VALID = 'valid' VALID = 'valid'
@ -12,7 +9,35 @@ INVALID = 'invalid'
UNDECIDED = 'undecided' UNDECIDED = 'undecided'
def partition_eligible_votes(votes, eligible_voters, verify_vote_signature): class Voting:
"""
Everything to do with creating and checking votes.
All functions in this class should be referentially transparent, that is,
they always give the same output for a given input. This makes it easier
to test. This also means no logging!
Assumptions regarding data:
* Vote is a dictionary, but it is not assumed that any properties are.
* Everything else is assumed to be structurally correct, otherwise errors
may be thrown.
"""
@classmethod
def block_election(cls, block, votes, keyring):
"""
Calculate the election status of a block.
"""
eligible_voters = set(block['voters']) & set(keyring)
eligible_votes, ineligible_votes = \
cls.partition_eligible_votes(votes, eligible_voters)
results = cls.count_votes(eligible_votes)
results['status'] = decide_votes(results['counts'])
results['ineligible'] = ineligible_votes
return results
@classmethod
def partition_eligible_votes(cls, votes, eligible_voters):
""" """
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. This is the primary Sybill protection. block. This is the primary Sybill protection.
@ -20,16 +45,16 @@ def partition_eligible_votes(votes, eligible_voters, verify_vote_signature):
eligible, ineligible = ([], []) eligible, ineligible = ([], [])
for vote in votes: for vote in votes:
voter_eligible = vote['node_pubkey'] in eligible_voters voter_eligible = vote.get('node_pubkey') in eligible_voters
if voter_eligible and verify_vote_signature(vote): if voter_eligible and cls.verify_vote_signature(vote):
eligible.append(vote) eligible.append(vote)
else: else:
ineligible.append(vote) ineligible.append(vote)
return eligible, ineligible return eligible, ineligible
@classmethod
def count_votes(eligible_votes): def count_votes(cls, eligible_votes):
""" """
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), count the votes to produce three quantities: as voters), count the votes to produce three quantities:
@ -48,15 +73,22 @@ def count_votes(eligible_votes):
n_valid = 0 n_valid = 0
n_invalid = 0 n_invalid = 0
prev_blocks = collections.Counter() prev_blocks = collections.Counter()
cheat = {} cheat = []
malformed = []
for pubkey, votes in by_voter.items(): for pubkey, votes in by_voter.items():
if len(votes) > 1: if len(votes) > 1:
cheat[pubkey] = votes cheat.append(votes)
n_invalid += 1 n_invalid += 1
continue continue
vote = votes[0] vote = votes[0]
if not cls.verify_vote_schema(vote):
malformed.append(vote)
n_invalid += 1
continue
prev_blocks[vote['vote']['previous_block']] += 1 prev_blocks[vote['vote']['previous_block']] += 1
if vote['vote']['is_block_valid']: if vote['vote']['is_block_valid']:
n_valid += 1 n_valid += 1
@ -64,13 +96,17 @@ def count_votes(eligible_votes):
n_invalid += 1 n_invalid += 1
return { return {
'counts': {
'n_valid': n_valid, 'n_valid': n_valid,
'n_invalid': n_invalid, 'n_invalid': n_invalid,
'n_agree_prev_block': prev_blocks.most_common()[0][1] 'n_agree_prev_block': prev_blocks.most_common()[0][1],
}, cheat },
'cheat': cheat,
'malformed': malformed,
}
@classmethod
def decide_votes(n_voters, n_valid, n_invalid, n_agree_prev_block): def decide_votes(cls, n_voters, n_valid, n_invalid, n_agree_prev_block):
""" """
Decide on votes. Decide on votes.
@ -97,3 +133,35 @@ def decide_votes(n_voters, n_valid, n_invalid, n_agree_prev_block):
return VALID return VALID
return INVALID return INVALID
return UNDECIDED return UNDECIDED
@classmethod
def verify_vote_signature(cls, vote):
"""Verify the signature of a vote
A valid vote should have been signed by a voter's private key.
Args:
vote (list): voters of the block that is under election
Returns:
bool: True if the signature is correct, False otherwise.
"""
signature = vote.get('signature')
pk_base58 = vote.get('node_pubkey')
if not (type(signature) == str and type(pk_base58) == str):
raise ValueError("Malformed vote: %s" % vote)
public_key = PublicKey(pk_base58)
body = serialize(signed_vote['vote']).encode()
return public_key.verify(body, signature)
@classmethod
def verify_vote_schema(cls, vote):
# I'm not sure this is the correct approach. Maybe we should allow
# duck typing w/r/t votes.
try:
validate_vote_schema(vote)
return True
except SchemaValidationError:
return False

View File

@ -1,33 +1,36 @@
import pytest import pytest
from unittest.mock import patch
from bigchaindb.core import Bigchain from bigchaindb.core import Bigchain
from bigchaindb.voting import (count_votes, partition_eligible_votes, from bigchaindb.voting import Voting, INVALID, VALID, UNDECIDED
decide_votes, INVALID, VALID, UNDECIDED)
################################################################################ ################################################################################
# Tests for checking vote eligibility # Tests for checking vote eligibility
def test_partition_eligible_votes(): @patch('bigchaindb.voting.Voting.verify_vote_signature')
def test_partition_eligible_votes(_):
nodes = list(map(Bigchain, 'abc')) nodes = list(map(Bigchain, 'abc'))
votes = [n.vote('block', 'a', True) for n in nodes] votes = [n.vote('block', 'a', True) for n in nodes]
el, inel = partition_eligible_votes(votes, 'abc', lambda _: True) el, inel = Voting.partition_eligible_votes(votes, 'abc')
assert el == votes assert el == votes
assert inel == [] assert inel == []
def test_count_votes(): @patch('bigchaindb.voting.Voting.verify_vote_schema')
def test_count_votes(_):
nodes = list(map(Bigchain, 'abc')) nodes = list(map(Bigchain, 'abc'))
votes = [n.vote('block', 'a', True) for n in nodes] votes = [n.vote('block', 'a', True) for n in nodes]
assert count_votes(votes) == ({ assert Voting.count_votes(votes)['counts'] == {
'n_valid': 3, 'n_valid': 3,
'n_invalid': 0, 'n_invalid': 0,
'n_agree_prev_block': 3 'n_agree_prev_block': 3
}, {}) }
################################################################################ ################################################################################
@ -53,26 +56,29 @@ DECISION_TESTS = [dict(
def test_decide_votes_valid(kwargs): def test_decide_votes_valid(kwargs):
kwargs = kwargs.copy() kwargs = kwargs.copy()
kwargs['n_invalid'] = 0 kwargs['n_invalid'] = 0
assert decide_votes(**kwargs) == VALID assert Voting.decide_votes(**kwargs) == VALID
kwargs['n_agree_prev_block'] -= 1 kwargs['n_agree_prev_block'] -= 1
assert decide_votes(**kwargs) == INVALID assert Voting.decide_votes(**kwargs) == INVALID
kwargs['n_valid'] -= 1 kwargs['n_valid'] -= 1
assert decide_votes(**kwargs) == UNDECIDED assert Voting.decide_votes(**kwargs) == UNDECIDED
@pytest.mark.parametrize('kwargs', DECISION_TESTS) @pytest.mark.parametrize('kwargs', DECISION_TESTS)
def test_decide_votes_invalid(kwargs): def test_decide_votes_invalid(kwargs):
kwargs = kwargs.copy() kwargs = kwargs.copy()
kwargs['n_valid'] = 0 kwargs['n_valid'] = 0
assert decide_votes(**kwargs) == INVALID assert Voting.decide_votes(**kwargs) == INVALID
kwargs['n_invalid'] -= 1 kwargs['n_invalid'] -= 1
assert decide_votes(**kwargs) == UNDECIDED assert Voting.decide_votes(**kwargs) == UNDECIDED
def test_decide_votes_checks_arguments(): def test_decide_votes_checks_arguments():
with pytest.raises(ValueError): with pytest.raises(ValueError):
decide_votes(n_voters=1, n_valid=2, n_invalid=0, n_agree_prev_block=0) Voting.decide_votes(n_voters=1, n_valid=2, n_invalid=0,
n_agree_prev_block=0)
with pytest.raises(ValueError): with pytest.raises(ValueError):
decide_votes(n_voters=1, n_valid=0, n_invalid=2, n_agree_prev_block=0) Voting.decide_votes(n_voters=1, n_valid=0, n_invalid=2,
n_agree_prev_block=0)
with pytest.raises(ValueError): with pytest.raises(ValueError):
decide_votes(n_voters=1, n_valid=0, n_invalid=0, n_agree_prev_block=2) Voting.decide_votes(n_voters=1, n_valid=0, n_invalid=0,
n_agree_prev_block=2)