diff --git a/tests/integration/test_federation.py b/tests/integration/test_federation.py new file mode 100644 index 00000000..c18c65de --- /dev/null +++ b/tests/integration/test_federation.py @@ -0,0 +1,215 @@ +from copy import deepcopy +import pytest +import random + +import bigchaindb +from bigchaindb.core import Bigchain +from contextlib import contextmanager +from bigchaindb.common.crypto import generate_key_pair +from tests.pipelines.stepping import create_stepper + + +################################################################################ +# Test setup code + + +@contextmanager +def federation(n): + """ + Return a list of Bigchain objects and pipeline steppers to represent + a BigchainDB federation + """ + keys = [generate_key_pair() for _ in range(n)] + config_orig = bigchaindb.config + + @contextmanager + def make_nodes(i): + nonlocal keys + if i == 0: + yield [] + else: + config = deepcopy(config_orig) + keys = [keys[-1]] + keys[:-1] + config['keypair']['private'] = keys[0][0] + config['keypair']['public'] = keys[0][1] + config['keyring'] = list(list(zip(*keys[1:]))[1]) + bigchaindb.config = config + stepper = create_stepper() + with stepper.start(): + node = (Bigchain(), stepper) + with make_nodes(i-1) as rest: + yield [node] + rest + + with make_nodes(n) as steppers: + bigchaindb.config = config_orig + yield zip(*steppers) + + +@pytest.fixture +def federation_3(): + with federation(3) as f: + yield f + + +def process_tx(steps): + steps.block_changefeed(timeout=1) + if steps.block_filter_tx(): + steps.block_validate_tx() + steps.block_create(timeout=True) + steps.block_write() + steps.block_delete_tx() + + +def input_single_create(b): + from bigchaindb.common.transaction import Transaction + metadata = {'r': random.random()} + tx = Transaction.create([b.me], [([b.me], 1)], metadata).sign([b.me_private]) + b.write_transaction(tx) + return tx + + +def process_vote(steps, result=None): + steps.vote_changefeed() + steps.vote_validate_block() + steps.vote_ungroup() + steps.vote_validate_tx() + if result is not None: + steps.queues['vote_vote'][0][0] = result + vote = steps.vote_vote() + steps.vote_write_vote() + return vote + + +################################################################################ +# Tests here on down + + +@pytest.mark.bdb +@pytest.mark.genesis +def test_elect_valid(federation_3): + [bx, (s0, s1, s2)] = federation_3 + tx = input_single_create(bx[0]) + process_tx(s0) + process_tx(s1) + process_tx(s2) + process_vote(s2, False) + for i in range(3): + assert bx[i].get_transaction(tx.id, True)[1] == 'undecided' + process_vote(s0, True) + for i in range(3): + assert bx[i].get_transaction(tx.id, True)[1] == 'undecided' + process_vote(s1, True) + for i in range(3): + assert bx[i].get_transaction(tx.id, True)[1] == 'valid' + + +@pytest.mark.bdb +@pytest.mark.genesis +def test_elect_invalid(federation_3): + [bx, (s0, s1, s2)] = federation_3 + tx = input_single_create(bx[0]) + process_tx(s0) + process_tx(s1) + process_tx(s2) + process_vote(s1, True) + for i in range(3): + assert bx[i].get_transaction(tx.id, True)[1] == 'undecided' + process_vote(s2, False) + for i in range(3): + assert bx[i].get_transaction(tx.id, True)[1] == 'undecided' + process_vote(s0, False) + for i in range(3): + assert bx[i].get_transaction(tx.id, True)[1] is None + + +@pytest.mark.bdb +@pytest.mark.genesis +def test_elect_disagree_prev_block(federation_3): + [bx, (s0, s1, s2)] = federation_3 + tx = input_single_create(bx[0]) + process_tx(s0) + process_tx(s1) + process_tx(s2) + process_vote(s0, True) + for i in range(3): + assert bx[i].get_transaction(tx.id, True)[1] == 'undecided' + s1.vote.last_voted_id = '5' * 64 + process_vote(s1, True) + for i in range(3): + assert bx[i].get_transaction(tx.id, True)[1] == 'undecided' + s2.vote.last_voted_id = '6' * 64 + process_vote(s2, True) + for i in range(3): + assert bx[i].get_transaction(tx.id, True)[1] is None + + +@pytest.mark.skip() # TODO: wait for #1309 +@pytest.mark.bdb +@pytest.mark.genesis +def test_elect_dupe_vote(federation_3): + from bigchaindb.exceptions import CriticalDuplicateVote + [bx, (s0, s1, s2)] = federation_3 + tx = input_single_create(bx[0]) + process_tx(s0) + process_tx(s1) + process_tx(s2) + vote = process_vote(s0, True) + # Drop the unique index and write the vote again + bx[0].connection.db.votes.drop_index('block_and_voter') + s0.queues['vote_write_vote'].append([vote]) + s0.vote_write_vote() + for i in range(3): + with pytest.raises(CriticalDuplicateVote): + bx[i].get_transaction(tx.id, True)[1] + + +@pytest.mark.bdb +@pytest.mark.genesis +def test_elect_sybill(federation_3): + [bx, (s0, s1, s2)] = federation_3 + tx = input_single_create(bx[0]) + process_tx(s0) + process_tx(s1) + process_tx(s2) + # What we need is some votes from unknown nodes! + for s in [s0, s1, s2]: + s.vote.bigchain.me_private = generate_key_pair()[0] + process_vote(s0, True) + process_vote(s1, True) + process_vote(s2, True) + for i in range(3): + assert bx[i].get_transaction(tx.id, True)[1] == 'undecided' + + +@pytest.mark.skip() +@pytest.mark.bdb +@pytest.mark.genesis +def test_elect_dos(federation_3): + """ + https://github.com/bigchaindb/bigchaindb/issues/1314 + Test that a node cannot block another node's opportunity to vote + on a block by writing an incorrectly signed vote + """ + raise NotImplementedError() + + +@pytest.mark.skip('Revisit when we have block election status cache') +@pytest.mark.bdb +@pytest.mark.genesis +def test_elect_bad_block_voters_list(federation_3): + """ + See https://github.com/bigchaindb/bigchaindb/issues/1224 + """ + [bx, (s0, s1, s2)] = federation_3 + b = s0.block.bigchain + # First remove other nodes from node 0 so that it self assigns the tx + b.nodes_except_me = [] + tx = input_single_create(b) + # Now create a block voters list which will not match other keyrings + b.nodes_except_me = [bx[1].me] + process_tx(s0) + process_vote(s0) + process_vote(s1) + process_vote(s2) + for i in range(3): + assert bx[i].get_transaction(tx.id, True)[1] == 'invalid' diff --git a/tests/pipelines/stepping.py b/tests/pipelines/stepping.py index 1a9d3a69..36f68a6a 100644 --- a/tests/pipelines/stepping.py +++ b/tests/pipelines/stepping.py @@ -163,6 +163,8 @@ def _update_stepper(stepper, prefix, pipeline): n1 = (nodes + [None])[i+1] f = stepper.add_input if i == 0 else stepper.add_stage f(prefix, n0, n1) + # Expose pipeline state + setattr(stepper, prefix, nodes[-1].target.__self__) def create_stepper():