bigchaindb/tests/db/test_voter.py
Ryan Henderson 9f03b2b771 add election class, process starts (#221)
* add election class, process starts

* remove else clause

* resolve splitvote

* move quorum check to core

* remove comments

* fixed vote counting

* add test

* poison pill

* queue test

* generalize election status

* get correct part of block

* clean shut down

* test block liquidation

* add vote signature checking

* add tests

* user_public_key to user_vk

* python3 style floordivs

* correct verdict for odd number of voters, and tests

* remove whitespace

* simplify verify_vote_signature

* election checks pubkey is in block voters

* tests for elector pubkey

* count invalid votes as votes for invalid block

* test update

* test mismatched vote, block sigs

* variable name change

* comments on voting logic

* comments on voting logic

* remove unused queue

* correct block model

* imperative docstring

* election status class constants

* election tests use constants
2016-05-19 14:03:52 +02:00

516 lines
18 KiB
Python

import pytest
import time
import rethinkdb as r
import multiprocessing as mp
from bigchaindb import util
from bigchaindb.voter import Voter, Election, BlockStream
from bigchaindb import crypto, Bigchain
class TestBigchainVoter(object):
def test_valid_block_voting(self, b):
q_new_block = mp.Queue()
genesis = b.create_genesis_block()
# create valid block
block = b.create_block([])
# assert block is valid
assert b.is_valid_block(block)
b.write_block(block, durability='hard')
# create queue and voter
voter = Voter(q_new_block)
# vote
voter.start()
# wait for vote to be written
time.sleep(1)
voter.kill()
# retrive block from bigchain
blocks = list(r.table('bigchain')
.order_by(r.asc((r.row['block']['timestamp'])))
.run(b.conn))
# validate vote
assert len(blocks[1]['votes']) == 1
vote = blocks[1]['votes'][0]
assert vote['vote']['voting_for_block'] == block['id']
assert vote['vote']['previous_block'] == genesis['id']
assert vote['vote']['is_block_valid'] is True
assert vote['vote']['invalid_reason'] is None
assert vote['node_pubkey'] == b.me
assert crypto.VerifyingKey(b.me).verify(util.serialize(vote['vote']), vote['signature']) is True
def test_valid_block_voting_with_create_transaction(self, b):
q_new_block = mp.Queue()
genesis = b.create_genesis_block()
# create a `CREATE` transaction
test_user_priv, test_user_pub = crypto.generate_key_pair()
tx = b.create_transaction(b.me, test_user_pub, None, 'CREATE')
tx_signed = b.sign_transaction(tx, b.me_private)
assert b.is_valid_transaction(tx_signed)
# create valid block
block = b.create_block([tx_signed])
# assert block is valid
assert b.is_valid_block(block)
b.write_block(block, durability='hard')
# create queue and voter
voter = Voter(q_new_block)
# vote
voter.start()
# wait for vote to be written
time.sleep(1)
voter.kill()
# retrive block from bigchain
blocks = list(r.table('bigchain')
.order_by(r.asc((r.row['block']['timestamp'])))
.run(b.conn))
# validate vote
assert len(blocks[1]['votes']) == 1
vote = blocks[1]['votes'][0]
assert vote['vote']['voting_for_block'] == block['id']
assert vote['vote']['previous_block'] == genesis['id']
assert vote['vote']['is_block_valid'] is True
assert vote['vote']['invalid_reason'] is None
assert vote['node_pubkey'] == b.me
assert crypto.VerifyingKey(b.me).verify(util.serialize(vote['vote']), vote['signature']) is True
def test_valid_block_voting_with_transfer_transactions(self, b):
q_new_block = mp.Queue()
b.create_genesis_block()
# create a `CREATE` transaction
test_user_priv, test_user_pub = crypto.generate_key_pair()
tx = b.create_transaction(b.me, test_user_pub, None, 'CREATE')
tx_signed = b.sign_transaction(tx, b.me_private)
assert b.is_valid_transaction(tx_signed)
# create valid block
block = b.create_block([tx_signed])
# assert block is valid
assert b.is_valid_block(block)
b.write_block(block, durability='hard')
# create queue and voter
voter = Voter(q_new_block)
# vote
voter.start()
# wait for vote to be written
time.sleep(1)
voter.kill()
# retrive block from bigchain
blocks = list(r.table('bigchain')
.order_by(r.asc((r.row['block']['timestamp'])))
.run(b.conn))
# validate vote
assert len(blocks[1]['votes']) == 1
# create a `TRANSFER` transaction
test_user2_priv, test_user2_pub = crypto.generate_key_pair()
tx2 = b.create_transaction(test_user_pub, test_user2_pub, {'txid': tx['id'], 'cid': 0}, 'TRANSFER')
tx2_signed = b.sign_transaction(tx2, test_user_priv)
assert b.is_valid_transaction(tx2_signed)
# create valid block
block = b.create_block([tx2_signed])
# assert block is valid
assert b.is_valid_block(block)
b.write_block(block, durability='hard')
# create queue and voter
voter = Voter(q_new_block)
# vote
voter.start()
# wait for vote to be written
time.sleep(1)
voter.kill()
# retrive block from bigchain
blocks = list(r.table('bigchain')
.order_by(r.asc((r.row['block']['timestamp'])))
.run(b.conn))
# validate vote
assert len(blocks[2]['votes']) == 1
vote = blocks[2]['votes'][0]
assert vote['vote']['voting_for_block'] == block['id']
assert vote['vote']['is_block_valid'] is True
assert vote['vote']['invalid_reason'] is None
assert vote['node_pubkey'] == b.me
assert crypto.VerifyingKey(b.me).verify(util.serialize(vote['vote']), vote['signature']) is True
def test_invalid_block_voting(self, b, user_vk):
# create queue and voter
q_new_block = mp.Queue()
voter = Voter(q_new_block)
# create transaction
transaction = b.create_transaction(b.me, user_vk, None, 'CREATE')
transaction_signed = b.sign_transaction(transaction, b.me_private)
genesis = b.create_genesis_block()
# create invalid block
block = b.create_block([transaction_signed])
# change transaction id to make it invalid
block['block']['transactions'][0]['id'] = 'abc'
assert not b.is_valid_block(block)
b.write_block(block, durability='hard')
# vote
voter.start()
time.sleep(1)
voter.kill()
# retrive block from bigchain
blocks = list(r.table('bigchain')
.order_by(r.asc((r.row['block']['timestamp'])))
.run(b.conn))
# validate vote
assert len(blocks[1]['votes']) == 1
vote = blocks[1]['votes'][0]
assert vote['vote']['voting_for_block'] == block['id']
assert vote['vote']['previous_block'] == genesis['id']
assert vote['vote']['is_block_valid'] is False
assert vote['vote']['invalid_reason'] is None
assert vote['node_pubkey'] == b.me
assert crypto.VerifyingKey(b.me).verify(util.serialize(vote['vote']), vote['signature']) is True
def test_vote_creation_valid(self, b):
# create valid block
block = b.create_block([])
# retrieve vote
vote = b.vote(block, 'abc', True)
# assert vote is correct
assert vote['vote']['voting_for_block'] == block['id']
assert vote['vote']['previous_block'] == 'abc'
assert vote['vote']['is_block_valid'] is True
assert vote['vote']['invalid_reason'] is None
assert vote['node_pubkey'] == b.me
assert crypto.VerifyingKey(b.me).verify(util.serialize(vote['vote']), vote['signature']) is True
def test_vote_creation_invalid(self, b):
# create valid block
block = b.create_block([])
# retrieve vote
vote = b.vote(block, 'abc', False)
# assert vote is correct
assert vote['vote']['voting_for_block'] == block['id']
assert vote['vote']['previous_block'] == 'abc'
assert vote['vote']['is_block_valid'] is False
assert vote['vote']['invalid_reason'] is None
assert vote['node_pubkey'] == b.me
assert crypto.VerifyingKey(b.me).verify(util.serialize(vote['vote']), vote['signature']) is True
def test_voter_considers_unvoted_blocks_when_single_node(self, b):
# simulate a voter going donw in a single node environment
b.create_genesis_block()
# insert blocks in the database while the voter process is not listening
# (these blocks won't appear in the changefeed)
block_1 = b.create_block([])
b.write_block(block_1, durability='hard')
block_2 = b.create_block([])
b.write_block(block_2, durability='hard')
# voter is back online, we simulate that by creating a queue and a Voter instance
q_new_block = mp.Queue()
voter = Voter(q_new_block)
# create a new block that will appear in the changefeed
block_3 = b.create_block([])
b.write_block(block_3, durability='hard')
# put the last block in the queue
q_new_block.put(block_3)
# vote
voter.start()
time.sleep(1)
voter.kill()
# retrive blocks from bigchain
blocks = list(r.table('bigchain')
.order_by(r.asc((r.row['block']['timestamp'])))
.run(b.conn))
# FIXME: remove genesis block, we don't vote on it (might change in the future)
blocks.pop(0)
assert all(block['votes'][0]['node_pubkey'] == b.me for block in blocks)
def test_voter_chains_blocks_with_the_previous_ones(self, b):
b.create_genesis_block()
block_1 = b.create_block([])
b.write_block(block_1, durability='hard')
block_2 = b.create_block([])
b.write_block(block_2, durability='hard')
q_new_block = mp.Queue()
voter = Voter(q_new_block)
voter.start()
time.sleep(1)
voter.kill()
# retrive blocks from bigchain
blocks = list(r.table('bigchain')
.order_by(r.asc((r.row['block']['timestamp'])))
.run(b.conn))
assert blocks[0]['block_number'] == 0
assert blocks[1]['block_number'] == 1
assert blocks[2]['block_number'] == 2
# we don't vote on the genesis block right now
# assert blocks[0]['votes'][0]['vote']['voting_for_block'] == genesis['id']
assert blocks[1]['votes'][0]['vote']['voting_for_block'] == block_1['id']
assert blocks[2]['votes'][0]['vote']['voting_for_block'] == block_2['id']
@pytest.mark.skipif(reason='Updating the block_number must be atomic')
def test_updating_block_number_must_be_atomic(self):
pass
class TestBlockElection(object):
def test_quorum(self, b):
# create a new block
test_block = b.create_block([])
# 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]
# dummy block with test federation public keys as voters
test_block['block']['voters'] = [key_pair[1] for key_pair in key_pairs]
# fake "yes" votes
valid_vote = [member.vote(test_block, 'abc', True)
for member in test_federation]
# fake "no" votes
invalid_vote = [member.vote(test_block, 'abc', False)
for member in test_federation]
# fake "yes" votes with incorrect signatures
improperly_signed_valid_vote = [member.vote(test_block, 'abc', True) for
member in test_federation]
[vote['vote'].update(this_should_ruin_things='lol')
for vote in improperly_signed_valid_vote]
# test unanimously valid block
test_block['votes'] = valid_vote
assert b.block_election_status(test_block) == Bigchain.BLOCK_VALID
# test partial quorum situations
test_block['votes'] = valid_vote[:2]
assert b.block_election_status(test_block) == Bigchain.BLOCK_UNDECIDED
#
test_block['votes'] = valid_vote[:3]
assert b.block_election_status(test_block) == Bigchain.BLOCK_VALID
#
test_block['votes'] = invalid_vote[:2]
assert b.block_election_status(test_block) == Bigchain.BLOCK_INVALID
# test unanimously valid block with one improperly signed vote -- should still succeed
test_block['votes'] = valid_vote[:3] + improperly_signed_valid_vote[:1]
assert b.block_election_status(test_block) == Bigchain.BLOCK_VALID
# test unanimously valid block with two improperly signed votes -- should fail
test_block['votes'] = valid_vote[:2] + improperly_signed_valid_vote[:2]
assert b.block_election_status(test_block) == Bigchain.BLOCK_INVALID
# test block with minority invalid vote
test_block['votes'] = invalid_vote[:1] + valid_vote[:3]
assert b.block_election_status(test_block) == Bigchain.BLOCK_VALID
# test split vote
test_block['votes'] = invalid_vote[:2] + valid_vote[:2]
assert b.block_election_status(test_block) == Bigchain.BLOCK_INVALID
# test undecided
test_block['votes'] = valid_vote[:2]
assert b.block_election_status(test_block) == Bigchain.BLOCK_UNDECIDED
# change signatures in block, should fail
test_block['block']['voters'][0] = 'abc'
test_block['block']['voters'][1] = 'abc'
test_block['votes'] = valid_vote
assert b.block_election_status(test_block) == Bigchain.BLOCK_INVALID
def test_quorum_odd(self, b):
# test partial quorum situations for odd numbers of voters
# create a new block
test_block = b.create_block([])
# simulate a federation with four voters
key_pairs = [crypto.generate_key_pair() for _ in range(5)]
test_federation = [Bigchain(public_key=key_pair[1], private_key=key_pair[0])
for key_pair in key_pairs]
# dummy block with test federation public keys as voters
test_block['block']['voters'] = [key_pair[1] for key_pair in key_pairs]
# fake "yes" votes
valid_vote = [member.vote(test_block, 'abc', True)
for member in test_federation]
# fake "no" votes
invalid_vote = [member.vote(test_block, 'abc', False)
for member in test_federation]
test_block['votes'] = valid_vote[:2]
assert b.block_election_status(test_block) == Bigchain.BLOCK_UNDECIDED
test_block['votes'] = invalid_vote[:2]
assert b.block_election_status(test_block) == Bigchain.BLOCK_UNDECIDED
test_block['votes'] = valid_vote[:3]
assert b.block_election_status(test_block) == Bigchain.BLOCK_VALID
test_block['votes'] = invalid_vote[:3]
assert b.block_election_status(test_block) == Bigchain.BLOCK_INVALID
def test_tx_rewritten_after_invalid(self, b, user_vk):
q_block_new_vote = mp.Queue()
# create blocks with transactions
tx1 = b.create_transaction(b.me, user_vk, None, 'CREATE')
tx2 = b.create_transaction(b.me, user_vk, None, 'CREATE')
test_block_1 = b.create_block([tx1])
test_block_2 = b.create_block([tx2])
# 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]
# simulate a federation with four voters
test_block_1['block']['voters'] = [key_pair[1] for key_pair in key_pairs]
test_block_2['block']['voters'] = [key_pair[1] for key_pair in key_pairs]
# votes for block one
vote_1 = [member.vote(test_block_1, 'abc', True)
for member in test_federation]
# votes for block two
vote_2 = [member.vote(test_block_2, 'abc', True) for member in test_federation[:2]] + \
[member.vote(test_block_2, 'abc', False) for member in test_federation[2:]]
# construct valid block
test_block_1['votes'] = vote_1
q_block_new_vote.put(test_block_1)
# construct invalid block
test_block_2['votes'] = vote_2
q_block_new_vote.put(test_block_2)
election = Election(q_block_new_vote)
election.start()
time.sleep(1)
election.kill()
# tx1 was in a valid block, and should not be in the backlog
assert r.table('backlog').get(tx1['id']).run(b.conn) is None
# tx2 was in an invalid block and SHOULD be in the backlog
assert r.table('backlog').get(tx2['id']).run(b.conn)['id'] == tx2['id']
class TestBlockStream(object):
def test_if_federation_size_is_greater_than_one_ignore_past_blocks(self, b):
for _ in range(5):
b.federation_nodes.append(crypto.generate_key_pair()[1])
new_blocks = mp.Queue()
bs = BlockStream(new_blocks)
block_1 = b.create_block([])
new_blocks.put(block_1)
assert block_1 == bs.get()
def test_if_no_old_blocks_get_should_return_new_blocks(self, b):
new_blocks = mp.Queue()
bs = BlockStream(new_blocks)
# create two blocks
block_1 = b.create_block([])
block_2 = b.create_block([])
# write the blocks
b.write_block(block_1, durability='hard')
b.write_block(block_2, durability='hard')
# simulate a changefeed
new_blocks.put(block_1)
new_blocks.put(block_2)
# and check if we get exactly these two blocks
assert bs.get() == block_1
assert bs.get() == block_2
def test_if_old_blocks_get_should_return_old_block_first(self, b):
# create two blocks
block_1 = b.create_block([])
block_2 = b.create_block([])
# write the blocks
b.write_block(block_1, durability='hard')
b.write_block(block_2, durability='hard')
new_blocks = mp.Queue()
bs = BlockStream(new_blocks)
# assert len(list(bs.old_blocks)) == 2
# import pdb; pdb.set_trace()
# from pprint import pprint as pp
# pp(bs.old_blocks)
# pp(block_1)
# pp(block_2)
# create two new blocks that will appear in the changefeed
block_3 = b.create_block([])
block_4 = b.create_block([])
# simulate a changefeed
new_blocks.put(block_3)
new_blocks.put(block_4)
assert len(bs.unvoted_blocks) == 2
# and check if we get the old blocks first
assert bs.get() == block_1
assert bs.get() == block_2
assert bs.get() == block_3
assert bs.get() == block_4
@pytest.mark.skipif(reason='We may have duplicated blocks when retrieving the BlockStream')
def test_ignore_duplicated_blocks_when_retrieving_the_blockstream(self):
pass