152 lines
4.9 KiB
Python
152 lines
4.9 KiB
Python
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
|
|
|
|
|
|
VALID = 'valid'
|
|
INVALID = 'invalid'
|
|
UNDECIDED = 'undecided'
|
|
|
|
|
|
class Voting:
|
|
"""
|
|
Everything to do with verifying and counting votes for block election.
|
|
|
|
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 no assumptions are made on it's properties.
|
|
* 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['block']['voters']) & set(keyring)
|
|
n_voters = len(eligible_voters)
|
|
eligible_votes, ineligible_votes = \
|
|
cls.partition_eligible_votes(votes, eligible_voters)
|
|
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
|
|
return results
|
|
|
|
@classmethod
|
|
def partition_eligible_votes(cls, votes, eligible_voters):
|
|
"""
|
|
Filter votes from unknown nodes or nodes that are not listed on
|
|
block. This is the primary Sybill protection.
|
|
"""
|
|
eligible, ineligible = ([], [])
|
|
|
|
for vote in votes:
|
|
voter_eligible = vote.get('node_pubkey') in eligible_voters
|
|
if voter_eligible:
|
|
try:
|
|
if cls.verify_vote_signature(vote):
|
|
eligible.append(vote)
|
|
continue
|
|
except ValueError:
|
|
pass
|
|
ineligible.append(vote)
|
|
return eligible, ineligible
|
|
|
|
@classmethod
|
|
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. Votes must agree on previous block, otherwise they become invalid.
|
|
"""
|
|
prev_blocks = collections.Counter()
|
|
malformed = []
|
|
|
|
for vote in by_voter.values():
|
|
if not cls.verify_vote_schema(vote):
|
|
malformed.append(vote)
|
|
continue
|
|
|
|
if vote['vote']['is_block_valid'] is True:
|
|
prev_blocks[vote['vote']['previous_block']] += 1
|
|
|
|
n_valid = 0
|
|
prev_block = None
|
|
# Valid votes must agree on previous block
|
|
if prev_blocks:
|
|
prev_block, n_valid = prev_blocks.most_common()[0]
|
|
del prev_blocks[prev_block]
|
|
|
|
return {
|
|
'counts': {
|
|
'n_valid': n_valid,
|
|
'n_invalid': len(by_voter) - n_valid,
|
|
},
|
|
'malformed': malformed,
|
|
'previous_block': prev_block,
|
|
'other_previous_block': dict(prev_blocks),
|
|
}
|
|
|
|
@classmethod
|
|
def decide_votes(cls, n_voters, n_valid, n_invalid):
|
|
"""
|
|
Decide on votes.
|
|
|
|
To return VALID there must be a clear majority that say VALID
|
|
and also agree on the previous block.
|
|
|
|
A tie on an even number of votes counts as INVALID.
|
|
"""
|
|
if n_invalid * 2 >= n_voters:
|
|
return INVALID
|
|
if n_valid * 2 > n_voters:
|
|
return VALID
|
|
return UNDECIDED
|
|
|
|
@classmethod
|
|
def verify_vote_signature(cls, vote):
|
|
"""
|
|
Verify the signature of a vote
|
|
"""
|
|
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(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 as e:
|
|
return False
|