bigchaindb/tests/db/test_voter.py
2016-08-01 14:55:37 +02:00

509 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, BlockStream
from bigchaindb import crypto, Bigchain
# Some util functions
def dummy_tx():
b = Bigchain()
tx = b.create_transaction(b.me, b.me, None, 'CREATE')
tx_signed = b.sign_transaction(tx, b.me_private)
return tx_signed
def dummy_block():
b = Bigchain()
block = b.create_block([dummy_tx()])
return block
class TestBigchainVoter(object):
def test_valid_block_voting(self, b):
q_new_block = mp.Queue()
genesis = b.create_genesis_block()
# create valid block
# sleep so that `block` as a higher timestamp then genesis
time.sleep(1)
block = dummy_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))
# retrieve vote
vote = r.table('votes').get_all([block['id'], b.me], index='block_and_voter').run(b.conn)
vote = vote.next()
# validate vote
assert vote is not None
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
# sleep so that block as a higher timestamp then genesis
time.sleep(1)
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))
# retrieve vote
vote = r.table('votes').get_all([block['id'], b.me], index='block_and_voter').run(b.conn)
vote = vote.next()
# validate vote
assert vote is not None
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))
# retrieve vote
vote = r.table('votes').get_all([block['id'], b.me], index='block_and_voter').run(b.conn)
vote = vote.next()
# validate vote
assert vote is not None
# 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))
# retrieve vote
vote = r.table('votes').get_all([blocks[2]['id'], b.me], index='block_and_voter').run(b.conn)
vote = vote.next()
# validate vote
assert vote is not None
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
# sleep so that `block` as a higher timestamp then `genesis`
time.sleep(1)
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))
# retrieve vote
vote = r.table('votes').get_all([block['id'], b.me], index='block_and_voter').run(b.conn)
vote = vote.next()
# validate vote
assert vote is not None
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 = dummy_block()
# retrieve vote
vote = b.vote(block['id'], '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 = dummy_block()
# retrieve vote
vote = b.vote(block['id'], '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 = dummy_block()
b.write_block(block_1, durability='hard')
block_2 = dummy_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)
# vote
voter.start()
time.sleep(1)
# create a new block that will appear in the changefeed
block_3 = dummy_block()
b.write_block(block_3, durability='hard')
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)
# retrieve vote
votes = r.table('votes').run(b.conn)
votes = list(votes)
assert all(vote['node_pubkey'] == b.me for vote in votes)
def test_voter_chains_blocks_with_the_previous_ones(self, b):
b.create_genesis_block()
# sleep so that `block_*` as a higher timestamp then `genesis`
time.sleep(1)
block_1 = dummy_block()
b.write_block(block_1, durability='hard')
time.sleep(1)
block_2 = dummy_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))
# retrieve votes
votes = list(r.table('votes').run(b.conn))
assert votes[0]['vote']['voting_for_block'] in (blocks[1]['id'], blocks[2]['id'])
assert votes[1]['vote']['voting_for_block'] in (blocks[1]['id'], blocks[2]['id'])
def test_voter_checks_for_previous_vote(self, b):
b.create_genesis_block()
block_1 = dummy_block()
b.write_block(block_1, durability='hard')
q_new_block = mp.Queue()
voter = Voter(q_new_block)
voter.start()
time.sleep(1)
retrieved_block = r.table('bigchain').get(block_1['id']).run(b.conn)
# queue block for voting AGAIN
q_new_block.put(retrieved_block)
time.sleep(1)
voter.kill()
re_retrieved_block = r.table('bigchain').get(block_1['id']).run(b.conn)
# block should be unchanged
assert retrieved_block == re_retrieved_block
@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 = dummy_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['id'], 'abc', True)
for member in test_federation]
# fake "no" votes
invalid_vote = [member.vote(test_block['id'], 'abc', False)
for member in test_federation]
# fake "yes" votes with incorrect signatures
improperly_signed_valid_vote = [member.vote(test_block['id'], '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
r.table('votes').insert(valid_vote, durability='hard').run(b.conn)
assert b.block_election_status(test_block) == Bigchain.BLOCK_VALID
r.table('votes').delete().run(b.conn)
# test partial quorum situations
r.table('votes').insert(valid_vote[:2], durability='hard').run(b.conn)
assert b.block_election_status(test_block) == Bigchain.BLOCK_UNDECIDED
r.table('votes').delete().run(b.conn)
#
r.table('votes').insert(valid_vote[:3], durability='hard').run(b.conn)
assert b.block_election_status(test_block) == Bigchain.BLOCK_VALID
r.table('votes').delete().run(b.conn)
#
r.table('votes').insert(invalid_vote[:2], durability='hard').run(b.conn)
assert b.block_election_status(test_block) == Bigchain.BLOCK_INVALID
r.table('votes').delete().run(b.conn)
# test unanimously valid block with one improperly signed vote -- should still succeed
r.table('votes').insert(valid_vote[:3] + improperly_signed_valid_vote[3:], durability='hard').run(b.conn)
assert b.block_election_status(test_block) == Bigchain.BLOCK_VALID
r.table('votes').delete().run(b.conn)
# test unanimously valid block with two improperly signed votes -- should fail
r.table('votes').insert(valid_vote[:2] + improperly_signed_valid_vote[2:], durability='hard').run(b.conn)
assert b.block_election_status(test_block) == Bigchain.BLOCK_INVALID
r.table('votes').delete().run(b.conn)
# test block with minority invalid vote
r.table('votes').insert(invalid_vote[:1] + valid_vote[1:], durability='hard').run(b.conn)
assert b.block_election_status(test_block) == Bigchain.BLOCK_VALID
r.table('votes').delete().run(b.conn)
# test split vote
r.table('votes').insert(invalid_vote[:2] + valid_vote[2:], durability='hard').run(b.conn)
assert b.block_election_status(test_block) == Bigchain.BLOCK_INVALID
r.table('votes').delete().run(b.conn)
# test undecided
r.table('votes').insert(valid_vote[:2], durability='hard').run(b.conn)
assert b.block_election_status(test_block) == Bigchain.BLOCK_UNDECIDED
r.table('votes').delete().run(b.conn)
# change signatures in block, should fail
test_block['block']['voters'][0] = 'abc'
test_block['block']['voters'][1] = 'abc'
r.table('votes').insert(valid_vote, durability='hard').run(b.conn)
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 = dummy_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['id'], 'abc', True)
for member in test_federation]
# fake "no" votes
invalid_vote = [member.vote(test_block['id'], 'abc', False)
for member in test_federation]
r.table('votes').insert(valid_vote[:2], durability='hard').run(b.conn)
assert b.block_election_status(test_block) == Bigchain.BLOCK_UNDECIDED
r.table('votes').delete().run(b.conn)
r.table('votes').insert(invalid_vote[:2], durability='hard').run(b.conn)
assert b.block_election_status(test_block) == Bigchain.BLOCK_UNDECIDED
r.table('votes').delete().run(b.conn)
r.table('votes').insert(valid_vote[:3], durability='hard').run(b.conn)
assert b.block_election_status(test_block) == Bigchain.BLOCK_VALID
r.table('votes').delete().run(b.conn)
r.table('votes').insert(invalid_vote[:3], durability='hard').run(b.conn)
assert b.block_election_status(test_block) == Bigchain.BLOCK_INVALID
r.table('votes').delete().run(b.conn)
class TestBlockStream(object):
def test_if_federation_size_is_greater_than_one_ignore_past_blocks(self, b):
for _ in range(5):
b.nodes_except_me.append(crypto.generate_key_pair()[1])
new_blocks = mp.Queue()
bs = BlockStream(new_blocks)
block_1 = dummy_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 = dummy_block()
block_2 = dummy_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
@pytest.mark.skipif(reason='We may have duplicated blocks when retrieving the BlockStream')
def test_ignore_duplicated_blocks_when_retrieving_the_blockstream(self):
pass