From efaf383732025e29c8781333796e1ee1a08a146e Mon Sep 17 00:00:00 2001 From: Ryan Henderson Date: Mon, 29 Aug 2016 14:24:09 +0200 Subject: [PATCH] check votes agree on previous block (#565) * check votes agree on previous block * documentation --- bigchaindb/core.py | 37 +++++++++++++++++++++++++++----- tests/pipelines/test_election.py | 27 +++++++++++++++++++++++ 2 files changed, 59 insertions(+), 5 deletions(-) diff --git a/bigchaindb/core.py b/bigchaindb/core.py index 6758ca0f..af265d09 100644 --- a/bigchaindb/core.py +++ b/bigchaindb/core.py @@ -1,8 +1,8 @@ import random import math -import operator import collections +from itertools import compress import rethinkdb as r import rapidjson @@ -696,15 +696,27 @@ class Bigchain(object): raise exceptions.MultipleVotesError('Block {block_id} has {n_votes} votes cast, but only {n_voters} voters' .format(block_id=block['id'], n_votes=str(len(votes)), n_voters=str(n_voters))) + # vote_cast is the list of votes e.g. [True, True, False] vote_cast = [vote['vote']['is_block_valid'] for vote in votes] + # prev_block are the ids of the nominal prev blocks e.g. + # ['block1_id', 'block1_id', 'block2_id'] + prev_block = [vote['vote']['previous_block'] for vote in votes] + # vote_validity checks whether a vote is valid + # or invalid, e.g. [False, True, True] vote_validity = [self.consensus.verify_vote_signature(block, vote) for vote in votes] # element-wise product of stated vote and validity of vote - vote_list = list(map(operator.mul, vote_cast, vote_validity)) + # vote_cast = [True, True, False] and + # vote_validity = [False, True, True] gives + # [True, False] + # Only the correctly signed votes are tallied. + vote_list = list(compress(vote_cast, vote_validity)) - # validate votes here + # Total the votes. Here, valid and invalid refer + # to the vote cast, not whether the vote itself + # is valid or invalid. n_valid_votes = sum(vote_list) - n_invalid_votes = len(vote_list) - n_valid_votes + n_invalid_votes = len(vote_cast) - n_valid_votes # The use of ceiling and floor is to account for the case of an # even number of voters where half the voters have voted 'invalid' @@ -714,6 +726,21 @@ class Bigchain(object): if n_invalid_votes >= math.ceil(n_voters / 2): return Bigchain.BLOCK_INVALID elif n_valid_votes > math.floor(n_voters / 2): - return Bigchain.BLOCK_VALID + # The block could be valid, but we still need to check if votes + # agree on the previous block. + # + # First, only consider blocks with legitimate votes + prev_block_list = list(compress(prev_block, vote_validity)) + # Next, only consider the blocks with 'yes' votes + prev_block_valid_list = list(compress(prev_block_list, vote_list)) + counts = collections.Counter(prev_block_valid_list) + # Make sure the majority vote agrees on previous node. + # The majority vote must be the most common, by definition. + # If it's not, there is no majority agreement on the previous + # block. + if counts.most_common()[0][1] > math.floor(n_voters / 2): + return Bigchain.BLOCK_VALID + else: + return Bigchain.BLOCK_INVALID else: return Bigchain.BLOCK_UNDECIDED diff --git a/tests/pipelines/test_election.py b/tests/pipelines/test_election.py index ad15e807..7d2835fa 100644 --- a/tests/pipelines/test_election.py +++ b/tests/pipelines/test_election.py @@ -36,6 +36,33 @@ def test_check_for_quorum_invalid(b, user_vk): assert e.check_for_quorum(votes[-1]) == test_block +def test_check_for_quorum_invalid_prev_node(b, user_vk): + e = election.Election() + + # create blocks with transactions + tx1 = b.create_transaction(b.me, user_vk, None, 'CREATE') + test_block = b.create_block([tx1]) + + # simulate a federation with four voters + key_pairs = [crypto.generate_key_pair() for _ in range(4)] + test_federation = [Bigchain(public_key=key_pair[1], private_key=key_pair[0]) + for key_pair in key_pairs] + + # add voters to block and write + test_block['block']['voters'] = [key_pair[1] for key_pair in key_pairs] + b.write_block(test_block) + + # split vote over prev node + votes = [member.vote(test_block['id'], 'abc', True) for member in test_federation[:2]] + \ + [member.vote(test_block['id'], 'def', True) for member in test_federation[2:]] + + # cast votes + r.table('votes').insert(votes, durability='hard').run(b.conn) + + # since nodes cannot agree on prev block, the block is invalid + assert e.check_for_quorum(votes[-1]) == test_block + + def test_check_for_quorum_valid(b, user_vk): e = election.Election()