mirror of
				https://github.com/bigchaindb/bigchaindb.git
				synced 2024-10-13 13:34:05 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			248 lines
		
	
	
		
			7.5 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			248 lines
		
	
	
		
			7.5 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| import pytest
 | |
| from unittest.mock import patch
 | |
| from collections import Counter
 | |
| 
 | |
| from bigchaindb.core import Bigchain
 | |
| from bigchaindb.exceptions import CriticalDuplicateVote
 | |
| from bigchaindb.voting import Voting, INVALID, VALID, UNDECIDED
 | |
| 
 | |
| 
 | |
| ################################################################################
 | |
| # Tests for checking vote eligibility
 | |
| 
 | |
| 
 | |
| def test_partition_eligible_votes():
 | |
|     class TestVoting(Voting):
 | |
|         @classmethod
 | |
|         def verify_vote_signature(cls, vote):
 | |
|             if vote['node_pubkey'] == 'invalid sig':
 | |
|                 return False
 | |
|             if vote['node_pubkey'] == 'value error':
 | |
|                 raise ValueError()
 | |
|             return True
 | |
| 
 | |
|     voters = ['valid', 'invalid sig', 'value error', 'not in set']
 | |
|     votes = [{'node_pubkey': k} for k in voters]
 | |
| 
 | |
|     el, inel = TestVoting.partition_eligible_votes(votes, voters[:-1])
 | |
|     assert el == [votes[0]]
 | |
|     assert inel == votes[1:]
 | |
| 
 | |
| 
 | |
| ################################################################################
 | |
| # Test vote counting
 | |
| 
 | |
| 
 | |
| def test_count_votes():
 | |
|     class TestVoting(Voting):
 | |
|         @classmethod
 | |
|         def verify_vote_schema(cls, vote):
 | |
|             return vote['node_pubkey'] != 'malformed'
 | |
| 
 | |
|     voters = (['says invalid', 'malformed'] +
 | |
|               ['kosher' + str(i) for i in range(10)])
 | |
| 
 | |
|     votes = [Bigchain(v).vote('block', 'a', True) for v in voters]
 | |
|     votes[0]['vote']['is_block_valid'] = False
 | |
|     # Incorrect previous block subtracts from n_valid and adds to n_invalid
 | |
|     votes[-1]['vote']['previous_block'] = 'z'
 | |
| 
 | |
|     by_voter = dict(enumerate(votes))
 | |
| 
 | |
|     assert TestVoting.count_votes(by_voter) == {
 | |
|         'counts': {
 | |
|             'n_valid': 9,    # 9 kosher votes
 | |
|             'n_invalid': 3,  # 1 invalid, 1 malformed, 1 rogue prev block
 | |
|         },
 | |
|         'malformed': [votes[1]],
 | |
|         'previous_block': 'a',
 | |
|         'other_previous_block': {'z': 1},
 | |
|     }
 | |
| 
 | |
| 
 | |
| def test_must_agree_prev_block():
 | |
|     class TestVoting(Voting):
 | |
|         @classmethod
 | |
|         def verify_vote_schema(cls, vote):
 | |
|             return True
 | |
| 
 | |
|     voters = 'abcd'
 | |
|     votes = [Bigchain(v).vote('block', 'a', True) for v in voters]
 | |
|     votes[0]['vote']['previous_block'] = 'b'
 | |
|     votes[1]['vote']['previous_block'] = 'c'
 | |
|     by_voter = dict(enumerate(votes))
 | |
|     assert TestVoting.count_votes(by_voter) == {
 | |
|         'counts': {
 | |
|             'n_valid': 2,
 | |
|             'n_invalid': 2,
 | |
|         },
 | |
|         'previous_block': 'a',
 | |
|         'other_previous_block': {'b': 1, 'c': 1},
 | |
|         'malformed': [],
 | |
|     }
 | |
| 
 | |
| 
 | |
| ################################################################################
 | |
| # Tests for vote decision making
 | |
| 
 | |
| 
 | |
| DECISION_TESTS = [
 | |
|     {'n_voters': 1, 'n_valid': 1, 'n_invalid': 1},
 | |
|     {'n_voters': 2, 'n_valid': 2, 'n_invalid': 1},
 | |
|     {'n_voters': 3, 'n_valid': 2, 'n_invalid': 2},
 | |
|     {'n_voters': 4, 'n_valid': 3, 'n_invalid': 2},
 | |
|     {'n_voters': 5, 'n_valid': 3, 'n_invalid': 3},
 | |
|     {'n_voters': 6, 'n_valid': 4, 'n_invalid': 3},
 | |
|     {'n_voters': 7, 'n_valid': 4, 'n_invalid': 4},
 | |
|     {'n_voters': 8, 'n_valid': 5, 'n_invalid': 4}
 | |
| ]
 | |
| 
 | |
| 
 | |
| @pytest.mark.parametrize('kwargs', DECISION_TESTS)
 | |
| def test_decide_votes_valid(kwargs):
 | |
|     kwargs = kwargs.copy()
 | |
|     kwargs['n_invalid'] = 0
 | |
|     assert Voting.decide_votes(**kwargs) == VALID
 | |
|     kwargs['n_valid'] -= 1
 | |
|     assert Voting.decide_votes(**kwargs) == UNDECIDED
 | |
| 
 | |
| 
 | |
| @pytest.mark.parametrize('kwargs', DECISION_TESTS)
 | |
| def test_decide_votes_invalid(kwargs):
 | |
|     kwargs = kwargs.copy()
 | |
|     kwargs['n_valid'] = 0
 | |
|     assert Voting.decide_votes(**kwargs) == INVALID
 | |
|     kwargs['n_invalid'] -= 1
 | |
|     assert Voting.decide_votes(**kwargs) == UNDECIDED
 | |
| 
 | |
| 
 | |
| ################################################################################
 | |
| # Actions - test state transitions
 | |
| 
 | |
| 
 | |
| @pytest.mark.parametrize('n_voters', range(8))
 | |
| def test_vote_actions(n_voters):
 | |
|     """* Legal transitions are UNDECIDED -> [VALID|INVALID] only
 | |
|     * Block is never left UNDECIDED after voting
 | |
|     * Accomodates rogues on previous block / invalid schema
 | |
|     """
 | |
|     class TestVoting(Voting):
 | |
|         @classmethod
 | |
|         def verify_vote_schema(cls, vote):
 | |
|             return type(vote['vote']['is_block_valid']) == bool
 | |
| 
 | |
|         @classmethod
 | |
|         def verify_vote_signature(cls, vote):
 | |
|             return True
 | |
| 
 | |
|     keyring = 'abcdefghijklmnopqrstuvwxyz'[:n_voters]
 | |
|     block = {'id': 'block', 'block': {'voters': keyring}}
 | |
|     state = UNDECIDED
 | |
|     todo = [(state, [], [])]
 | |
| 
 | |
|     def branch(p, r):
 | |
|         todo.append((state, votes, votes + [{
 | |
|             'node_pubkey': keyring[len(votes)],
 | |
|             'vote': {'previous_block': p, 'is_block_valid': r}
 | |
|         }]))
 | |
| 
 | |
|     while todo:
 | |
|         prev_state, prev_votes, votes = todo.pop(0)
 | |
|         results = Counter(v['vote']['is_block_valid'] for v in votes)
 | |
|         prev_blocks = Counter(v['vote']['previous_block'] for v in votes)
 | |
|         majority = n_voters // 2 + 1
 | |
|         honest = (len(votes) == majority and len(prev_blocks) == 1 and
 | |
|                   not results['lol'] and len(results) == 1)
 | |
|         closed = len(votes) == n_voters
 | |
| 
 | |
|         # Test legal transition
 | |
|         if votes:
 | |
|             state = TestVoting.block_election(block, votes, keyring)['status']
 | |
|             assert prev_state in [state, UNDECIDED]
 | |
| 
 | |
|         # Test that decision has been reached
 | |
|         if honest or closed:
 | |
|             assert state != UNDECIDED or n_voters == 0
 | |
| 
 | |
|         if closed:
 | |
|             continue
 | |
| 
 | |
|         # Can accomodate more votes, add them to the todo list.
 | |
|         # This vote is the good case
 | |
|         branch('A', True)
 | |
|         # This vote disagrees on previous block
 | |
|         branch('B', True)
 | |
|         # This vote says the block is invalid
 | |
|         branch('A', False)
 | |
|         # This vote is invalid
 | |
|         branch('A', 'lol')
 | |
| 
 | |
| 
 | |
| ################################################################################
 | |
| # Tests for vote signature
 | |
| 
 | |
| 
 | |
| def test_verify_vote_signature_passes(b):
 | |
|     vote = b.vote('block', 'a', True)
 | |
|     assert Voting.verify_vote_signature(vote)
 | |
|     vote['signature'] = ''
 | |
|     assert not Voting.verify_vote_signature(vote)
 | |
| 
 | |
| 
 | |
| ################################################################################
 | |
| # Tests for vote schema
 | |
| 
 | |
| 
 | |
| def test_verify_vote_schema(b):
 | |
|     vote = b.vote('b' * 64, 'a' * 64, True)
 | |
|     assert Voting.verify_vote_schema(vote)
 | |
|     vote = b.vote('b' * 64, 'a', True)
 | |
|     assert not Voting.verify_vote_schema(vote)
 | |
|     vote = b.vote('b', 'a' * 64, True)
 | |
|     assert not Voting.verify_vote_schema(vote)
 | |
| 
 | |
| 
 | |
| ################################################################################
 | |
| # block_election tests
 | |
| 
 | |
| 
 | |
| def test_block_election(b):
 | |
| 
 | |
|     class TestVoting(Voting):
 | |
|         @classmethod
 | |
|         def verify_vote_signature(cls, vote):
 | |
|             return True
 | |
| 
 | |
|         @classmethod
 | |
|         def verify_vote_schema(cls, vote):
 | |
|             return True
 | |
| 
 | |
|     keyring = 'abc'
 | |
|     block = {'id': 'xyz', 'block': {'voters': 'ab'}}
 | |
|     votes = [{
 | |
|         'node_pubkey': c,
 | |
|         'vote': {'is_block_valid': True, 'previous_block': 'a'}
 | |
|     } for c in 'abc']
 | |
| 
 | |
|     assert TestVoting.block_election(block, votes, keyring) == {
 | |
|         'status': VALID,
 | |
|         'block_id': 'xyz',
 | |
|         'counts': {'n_valid': 2, 'n_invalid': 0},
 | |
|         'ineligible': [votes[-1]],
 | |
|         'malformed': [],
 | |
|         'previous_block': 'a',
 | |
|         'other_previous_block': {},
 | |
|     }
 | |
| 
 | |
| 
 | |
| @patch('bigchaindb.voting.Voting.verify_vote_signature', return_value=True)
 | |
| def test_duplicate_vote_throws_critical_error(b):
 | |
|     keyring = 'abc'
 | |
|     block = {'id': 'xyz', 'block': {'voters': 'ab'}}
 | |
|     votes = [{
 | |
|         'node_pubkey': c,
 | |
|         'vote': {'is_block_valid': True, 'previous_block': 'a'}
 | |
|     } for c in 'aabc']
 | |
|     with pytest.raises(CriticalDuplicateVote):
 | |
|         Voting.block_election(block, votes, keyring)
 | 
