class TestChainController(unittest.TestCase): def setUp(self): self.blocks = BlockTreeManager() self.gossip = MockNetwork() self.executor = SynchronousExecutor() self.txn_executor = MockTransactionExecutor() self.block_sender = MockBlockSender() self.state_view_factory = MockStateViewFactory() def chain_updated(head, committed_batches=None, uncommitted_batches=None): pass self.chain_ctrl = ChainController( consensus_module=mock_consensus, block_cache=self.blocks.block_cache, state_view_factory=self.state_view_factory, block_sender=self.block_sender, executor=self.executor, transaction_executor=MockTransactionExecutor(), on_chain_updated=chain_updated, squash_handler=None) def test_simple_case(self): # TEST Run the simple case block_1 = self.blocks.generate_block(self.blocks.chain_head) self.chain_ctrl.on_block_received(block_1) self.executor.process_all() assert(self.chain_ctrl.chain_head.block.header_signature == block_1.header_signature) def test_alternate_genesis(self): # TEST Run generate and alternate genesis block head = self.chain_ctrl.chain_head for b in self.blocks.generate_chain(None, 5, {"add_to_cache": True}): self.chain_ctrl.on_block_received(b) self.executor.process_all() assert(self.chain_ctrl.chain_head.block.header_signature == head.block.header_signature) def test_bad_block_signature(self): # TEST Bad block extending current chain # Bad due to signature head = self.blocks.chain_head block_bad = self.blocks.generate_block(self.blocks.chain_head.block, invalid_signature=True) self.chain_ctrl.on_block_received(block_bad) assert (self.chain_ctrl.chain_head.block.header_signature == head.block.header_signature) def test_bad_block_consensus(self): # Bad due to consensus pass def test_bad_block_transaction(self): # Bad due to transaction pass
class TestChainController(unittest.TestCase): def setUp(self): self.blocks = BlockTreeManager() self.gossip = MockNetwork() self.executor = SynchronousExecutor() self.txn_executor = MockTransactionExecutor() self.block_sender = MockBlockSender() def chain_updated(head): pass self.chain_ctrl = ChainController( consensus=TestModeVerifier(), block_cache=self.blocks.block_cache, block_sender=self.block_sender, executor=self.executor, transaction_executor=MockTransactionExecutor(), on_chain_updated=chain_updated, squash_handler=None) def test_simple_case(self): # TEST Run the simple case block_1 = self.blocks.generate_block(self.blocks.chain_head) self.chain_ctrl.on_block_received(block_1) self.executor.process_all() assert(self.chain_ctrl.chain_head.block.header_signature == block_1.header_signature) def test_alternate_genesis(self): # TEST Run generate and alternate genesis block head = self.chain_ctrl.chain_head other_genesis = self.blocks.generate_block(add_to_store=True) for b in self.blocks.generate_chain(other_genesis, 5): self.chain_ctrl.on_block_received(b) self.executor.process_all() assert(self.chain_ctrl.chain_head.block.header_signature == head.block.header_signature) def test_bad_block_signature(self): # TEST Bad block extending current chain # Bad due to signature head = self.blocks.chain_head block_bad = self.blocks.generate_block(self.blocks.chain_head.block, invalid_signature=True) self.chain_ctrl.on_block_received(block_bad) assert (self.chain_ctrl.chain_head.block.header_signature == head.block.header_signature) def test_bad_block_consensus(self): # Bad due to consensus pass def test_bad_block_transaction(self): # Bad due to transaction pass
class TestChainController(unittest.TestCase): def setUp(self): self.blocks = BlockTreeManager() self.gossip = MockNetwork() self.executor = SynchronousExecutor() self.txn_executor = MockTransactionExecutor() self.block_sender = MockBlockSender() def chain_updated(head): pass self.chain_ctrl = ChainController( consensus=TestModeVerifier(), block_store=self.blocks.block_store, block_sender=self.block_sender, executor=self.executor, transaction_executor=MockTransactionExecutor(), on_chain_updated=chain_updated, squash_handler=None) def test_simple_case(self): # TEST Run the simple case block_1 = self.blocks.generate_block(self.blocks.chain_head) self.chain_ctrl.on_block_received(block_1.get_block()) self.executor.process_all() assert(self.chain_ctrl.chain_head.block.header_signature == block_1.header_signature) def test_alternate_genesis(self): # TEST Run generate and alternate genesis block head = self.chain_ctrl.chain_head other_genesis = self.blocks.generate_block(add_to_store=True) for b in self.blocks.generate_chain(other_genesis, 5): self.chain_ctrl.on_block_received(b.get_block()) self.executor.process_all() assert(self.chain_ctrl.chain_head.block.header_signature == head.block.header_signature) def test_bad_block_signature(self): # TEST Bad block extending current chain # Bad due to signature head = self.blocks.chain_head block_bad = self.blocks.generate_block(self.blocks.chain_head.block, invalid_signature=True) self.chain_ctrl.on_block_received(block_bad.get_block()) assert (self.chain_ctrl.chain_head.block.header_signature == head.block.header_signature) def test_bad_block_consensus(self): # Bad due to consensus pass def test_bad_block_transaction(self): # Bad due to transaction pass
def test_is_valid_subscription_known_old_chain(self): """Test that a check for a valid subscription where the known block id is a block in the middle of the chain should return True. """ mock_service = Mock() block_tree_manager = BlockTreeManager() chain = block_tree_manager.generate_chain( block_tree_manager.chain_head, 5) # Grab an id from the middle of the chain known_id = chain[3].identifier block_tree_manager.block_store.update_chain(chain) delta_store = StateDeltaStore(DictDatabase()) delta_processor = StateDeltaProcessor( service=mock_service, state_delta_store=delta_store, block_store=block_tree_manager.block_store) self.assertTrue(delta_processor.is_valid_subscription([known_id]))
class TestChainController(unittest.TestCase): def setUp(self): self.block_tree_manager = BlockTreeManager() self.gossip = MockNetwork() self.executor = SynchronousExecutor() self.txn_executor = MockTransactionExecutor() self.block_sender = MockBlockSender() self.chain_id_manager = MockChainIdManager() self._chain_head_lock = RLock() self.state_delta_processor = MockStateDeltaProcessor() def chain_updated(head, committed_batches=None, uncommitted_batches=None): pass self.chain_ctrl = ChainController( block_cache=self.block_tree_manager.block_cache, state_view_factory=MockStateViewFactory( self.block_tree_manager.state_db), block_sender=self.block_sender, executor=self.executor, transaction_executor=MockTransactionExecutor(), chain_head_lock=self._chain_head_lock, on_chain_updated=chain_updated, squash_handler=None, chain_id_manager=self.chain_id_manager, state_delta_processor=self.state_delta_processor, identity_signing_key=self.block_tree_manager.identity_signing_key, data_dir=None, config_dir=None) init_root = self.chain_ctrl.chain_head self.assert_is_chain_head(init_root) # create a chain of length 5 extending the root _, head = self.generate_chain(init_root, 5) self.receive_and_process_blocks(head) self.assert_is_chain_head(head) self.init_head = head def test_simple_case(self): new_block = self.generate_block(self.init_head) self.receive_and_process_blocks(new_block) self.assert_is_chain_head(new_block) # validate that the deltas for the new block are published self.assertEqual(new_block, self.state_delta_processor.block) def test_alternate_genesis(self): '''Tests a fork extending an alternate genesis block ''' chain, head = self.generate_chain(None, 5) for block in chain: self.receive_and_process_blocks(block) # make sure initial head is still chain head self.assert_is_chain_head(self.init_head) def test_bad_blocks(self): '''Tests bad blocks extending current chain ''' # Bad due to consensus bad_consen = self.generate_block( previous_block=self.init_head, invalid_consensus=True) # chain head should be the same self.receive_and_process_blocks(bad_consen) self.assert_is_chain_head(self.init_head) # Bad due to transaction bad_batch = self.generate_block( previous_block=self.init_head, invalid_batch=True) # chain head should be the same self.receive_and_process_blocks(bad_batch) self.assert_is_chain_head(self.init_head) # # Ensure good block works good_block = self.generate_block( previous_block=self.init_head) # chain head should be good_block self.receive_and_process_blocks(good_block) self.assert_is_chain_head(good_block) def test_fork_weights(self): '''Tests extending blocks of different weights ''' weight_4 = self.generate_block( previous_block=self.init_head, weight=4) weight_7 = self.generate_block( previous_block=self.init_head, weight=7) weight_8 = self.generate_block( previous_block=self.init_head, weight=8) self.receive_and_process_blocks( weight_7, weight_4, weight_8) self.assert_is_chain_head(weight_8) def test_fork_lengths(self): '''Tests competing forks of different lengths ''' _, head_2 = self.generate_chain(self.init_head, 2) _, head_7 = self.generate_chain(self.init_head, 7) _, head_5 = self.generate_chain(self.init_head, 5) self.receive_and_process_blocks( head_2, head_7, head_5) self.assert_is_chain_head(head_7) def test_advancing_chain(self): '''Tests the chain being advanced between a fork's creation and validation ''' _, fork_5 = self.generate_chain(self.init_head, 5) _, fork_3 = self.generate_chain(self.init_head, 3) self.receive_and_process_blocks(fork_3) self.assert_is_chain_head(fork_3) # fork_5 is longer than fork_3, so it should be accepted self.receive_and_process_blocks(fork_5) self.assert_is_chain_head(fork_5) def test_fork_missing_block(self): '''Tests a fork with a missing block ''' # make new chain new_chain, new_head = self.generate_chain(self.init_head, 5) self.chain_ctrl.on_block_received(new_head) # delete a block from the new chain del self.chain_ctrl._block_cache[new_chain[3].identifier] self.executor.process_all() # chain shouldn't advance self.assert_is_chain_head(self.init_head) # try again, chain still shouldn't advance self.receive_and_process_blocks(new_head) self.assert_is_chain_head(self.init_head) def test_fork_bad_block(self): '''Tests a fork with a bad block in the middle ''' # make two chains extending chain good_chain, good_head = self.generate_chain(self.init_head, 5) bad_chain, bad_head = self.generate_chain(self.init_head, 5) self.chain_ctrl.on_block_received(bad_head) self.chain_ctrl.on_block_received(good_head) # invalidate block in the middle of bad_chain bad_chain[3].status = BlockStatus.Invalid self.executor.process_all() # good_chain should be accepted self.assert_is_chain_head(good_head) def test_advancing_fork(self): '''Tests a fork advancing before getting validated ''' _, fork_head = self.generate_chain(self.init_head, 5) self.chain_ctrl.on_block_received(fork_head) # advance fork before it gets accepted _, ext_head = self.generate_chain(fork_head, 3) self.executor.process_all() self.assert_is_chain_head(fork_head) self.receive_and_process_blocks(ext_head) self.assert_is_chain_head(ext_head) def test_block_extends_in_validation(self): '''Tests a block getting extended while being validated ''' # create candidate block candidate = self.block_tree_manager.generate_block( previous_block=self.init_head) self.assert_is_chain_head(self.init_head) # queue up the candidate block, but don't process self.chain_ctrl.on_block_received(candidate) # create a new block extending the candidate block extending_block = self.block_tree_manager.generate_block( previous_block=candidate) self.assert_is_chain_head(self.init_head) # queue and process the extending block, # which should be the new head self.receive_and_process_blocks(extending_block) self.assert_is_chain_head(extending_block) def test_multiple_extended_forks(self): '''A more involved example of competing forks Three forks of varying lengths a_0, b_0, and c_0 are created extending the existing chain, with c_0 being the longest initially. The chains are extended in the following sequence: 1. Extend all forks by 2. The c fork should remain the head. 2. Extend forks by lenths such that the b fork is the longest. It should be the new head. 3. Extend all forks by 8. The b fork should remain the head. 4. Create a new fork of the initial chain longer than any of the other forks. It should be the new head. ''' # create forks of various lengths _, a_0 = self.generate_chain(self.init_head, 3) _, b_0 = self.generate_chain(self.init_head, 5) _, c_0 = self.generate_chain(self.init_head, 7) self.receive_and_process_blocks(a_0, b_0, c_0) self.assert_is_chain_head(c_0) # extend every fork by 2 _, a_1 = self.generate_chain(a_0, 2) _, b_1 = self.generate_chain(b_0, 2) _, c_1 = self.generate_chain(c_0, 2) self.receive_and_process_blocks(a_1, b_1, c_1) self.assert_is_chain_head(c_1) # extend the forks by different lengths _, a_2 = self.generate_chain(a_1, 1) _, b_2 = self.generate_chain(b_1, 6) _, c_2 = self.generate_chain(c_1, 3) self.receive_and_process_blocks(a_2, b_2, c_2) self.assert_is_chain_head(b_2) # extend every fork by 2 _, a_3 = self.generate_chain(a_2, 8) _, b_3 = self.generate_chain(b_2, 8) _, c_3 = self.generate_chain(c_2, 8) self.receive_and_process_blocks(a_3, b_3, c_3) self.assert_is_chain_head(b_3) # create a new longest chain _, wow = self.generate_chain(self.init_head, 30) self.receive_and_process_blocks(wow) self.assert_is_chain_head(wow) # next multi threaded # next add block publisher # next batch lists # integrate with LMDB # early vs late binding ( class member of consensus BlockPublisher) # helpers def assert_is_chain_head(self, block): chain_head_sig = self.chain_ctrl.chain_head.header_signature block_sig = block.header_signature self.assertEqual( chain_head_sig[:8], block_sig[:8], 'Not chain head') def generate_chain(self, root_block, num_blocks, params={'add_to_cache': True}): '''Returns (chain, chain_head). Usually only the head is needed, but occasionally the chain itself is used. ''' chain = self.block_tree_manager.generate_chain( root_block, num_blocks, params) head = chain[-1] return chain, head def generate_block(self, *args, **kwargs): return self.block_tree_manager.generate_block( *args, **kwargs) def receive_and_process_blocks(self, *blocks): for block in blocks: self.chain_ctrl.on_block_received(block) self.executor.process_all()
class TestBlockValidator(unittest.TestCase): def setUp(self): self.state_view_factory = MockStateViewFactory() self.block_tree_manager = BlockTreeManager() self.root = self.block_tree_manager.chain_head self.block_validation_handler = self.BlockValidationHandler() # fork based tests def test_fork_simple(self): """ Test a simple case of a new block extending the current root. """ new_block = self.block_tree_manager.generate_block( previous_block=self.root, add_to_store=True) self.validate_block(new_block) self.assert_valid_block(new_block) self.assert_new_block_committed() def test_good_fork_lower(self): """ Test case of a new block extending on a valid chain but not as long as the current chain. """ # create a new valid chain 5 long from the current root chain, head = self.generate_chain_with_head( self.root, 5, {'add_to_store': True}) self.block_tree_manager.set_chain_head(head) # generate candidate chain 3 long from the same root new_chain, new_head = self.generate_chain_with_head( self.root, 3, {'add_to_cache': True}) self.validate_block(new_head) self.assert_valid_block(new_head) self.assert_new_block_not_committed() def test_good_fork_higher(self): """ Test case of a new block extending on a valid chain but longer than the current chain. ( similar to test_good_fork_lower but uses a different code path when finding the common root ) """ # create a new valid chain 5 long from the current root chain, head = self.generate_chain_with_head( self.root, 5, {'add_to_store': True}) self.block_tree_manager.set_chain_head(head) # generate candidate chain 8 long from the same root new_chain, new_head = self.generate_chain_with_head( head, 8, {'add_to_cache': True}) self.validate_block(new_head) self.assert_valid_block(new_head) self.assert_new_block_committed() def test_fork_different_genesis(self): """" Test the case where new block is from a different genesis """ # create a new valid chain 5 long from the current root chain, head = self.generate_chain_with_head( self.root, 5, {'add_to_store': True}) self.block_tree_manager.set_chain_head(head) # generate candidate chain 5 long from its own genesis new_chain, new_head = self.generate_chain_with_head( None, 5, {'add_to_cache': True}) self.validate_block(new_head) self.assert_invalid_block(new_head) self.assert_new_block_not_committed() def test_fork_missing_predecessor(self): """" Test the case where new block is missing the a predecessor """ # generate candidate chain 5 long off the current head. chain, head = self.generate_chain_with_head( self.root, 5, {'add_to_cache': True}) # remove one of the new blocks del self.block_tree_manager.block_cache[chain[1].identifier] self.validate_block(head) self.assert_invalid_block(head) self.assert_new_block_not_committed() def test_fork_invalid_predecessor(self): """" Test the case where new block has an invalid predecessor """ # generate candidate chain 5 long off the current head. chain, head = self.generate_chain_with_head( self.root, 5, {'add_to_cache': True}) # Mark a predecessor as invalid chain[1].status = BlockStatus.Invalid self.validate_block(head) self.assert_invalid_block(head) self.assert_new_block_not_committed() def test_block_bad_consensus(self): """ Test the case where the new block has a bad batch """ chain, head = self.generate_chain_with_head( self.root, 5, {'add_to_store': True}) new_block = self.block_tree_manager.generate_block( previous_block=head, add_to_cache=True, invalid_consensus=True) self.validate_block(new_block) self.assert_invalid_block(new_block) self.assert_new_block_not_committed() def test_block_bad_batch(self): """ Test the case where the new block has a bad batch """ chain, head = self.generate_chain_with_head( self.root, 5, {'add_to_store': True}) new_block = self.block_tree_manager.generate_block( previous_block=head, add_to_cache=True, invalid_batch=True) self.validate_block(new_block) self.assert_invalid_block(new_block) self.assert_new_block_not_committed() def test_block_missing_batch(self): """ Test the case where the new block is missing a batch. """ pass def test_block_extra_batch(self): """ Test the case where the new block has an extra batch. """ pass def test_block_batches_order(self): """ Test the case where the new block has batches that are out of order. """ pass def test_block_missing_batch_dependency(self): """ Test the case where the new block has a batch that is missing a dependency. """ pass # assertions def assert_valid_block(self, block): self.assertEqual( block.status, BlockStatus.Valid, "Block should be valid") def assert_invalid_block(self, block): self.assertEqual( block.status, BlockStatus.Invalid, "Block should be invalid") def assert_new_block_committed(self): self.assert_handler_has_result() self.assertTrue( self.block_validation_handler.result["commit_new_block"], "New block not committed, should be") def assert_new_block_not_committed(self): self.assert_handler_has_result() self.assertFalse( self.block_validation_handler.result["commit_new_block"], "New block committed, shouldn't be") def assert_handler_has_result(self): msg = "Validation handler doesn't have result" self.assertTrue(self.block_validation_handler.has_result(), msg) # block validation def validate_block(self, block): validator = self.create_block_validator( block, self.block_validation_handler.on_block_validated) validator.run() def create_block_validator(self, new_block, on_block_validated): return BlockValidator( consensus_module=mock_consensus, new_block=new_block, chain_head=self.block_tree_manager.chain_head, state_view_factory=self.state_view_factory, block_cache=self.block_tree_manager.block_cache, done_cb=on_block_validated, executor=MockTransactionExecutor(), squash_handler=None, identity_signing_key=self.block_tree_manager.identity_signing_key, data_dir=None, config_dir=None) class BlockValidationHandler(object): def __init__(self): self.result = None def on_block_validated(self, commit_new_block, result): result["commit_new_block"] = commit_new_block self.result = result def has_result(self): return self.result is not None # block tree manager interface def generate_chain_with_head(self, root_block, num_blocks, params=None): chain = self.block_tree_manager.generate_chain( root_block, num_blocks, params) head = chain[-1] return chain, head
class TestBlockValidator(): def __init__(self): self.state_view_factory = MockStateViewFactory() self.block_tree_manager = BlockTreeManager() self.root = self.block_tree_manager.chain_head self.block_validation_handler = self.BlockValidationHandler() self.permission_verifier = MockPermissionVerifier() # assertions def assert_valid_block(self, block): assert block.status == BlockStatus.Valid def assert_invalid_block(self, block): assert block.status == BlockStatus.Invalid def assert_unknown_block(self, block): assert block.status == BlockStatus.Unknown def assert_new_block_committed(self): self.assert_handler_has_result() assert self.block_validation_handler.commit_new_block == True def assert_new_block_not_committed(self): self.assert_handler_has_result() assert self.block_validation_handler.commit_new_block == False def assert_handler_has_result(self): msg = "Validation handler doesn't have result" assert self.block_validation_handler.has_result() == True # block validation def validate_block(self, block): validator = self.create_block_validator() validator._load_consensus = lambda block: mock_consensus validator.process_block_verification( block, self.block_validation_handler.on_block_validated) def create_block_validator(self): return BlockValidator( state_view_factory=self.state_view_factory, block_cache=self.block_tree_manager.block_cache, transaction_executor=MockTransactionExecutor( batch_execution_result=None), squash_handler=None, identity_signer=self.block_tree_manager.identity_signer, data_dir=None, config_dir=None, permission_verifier=self.permission_verifier) class BlockValidationHandler(object): def __init__(self): self.commit_new_block = None self.result = None def on_block_validated(self, commit_new_block, result): self.commit_new_block = commit_new_block self.result = result def has_result(self): return not (self.result is None or self.commit_new_block is None) # block tree manager interface def generate_chain_with_head(self, root_block, num_blocks, params=None, exclude_head=True): chain = self.block_tree_manager.generate_chain(root_block, num_blocks, params, exclude_head) head = chain[-1] return chain, head
class TestChainController(): def __init__(self): self.block_tree_manager = BlockTreeManager() self.gossip = MockNetwork() self.txn_executor = MockTransactionExecutor() self.block_sender = MockBlockSender() self.chain_id_manager = MockChainIdManager() self._chain_head_lock = RLock() self.permission_verifier = MockPermissionVerifier() self.state_view_factory = MockStateViewFactory( self.block_tree_manager.state_db) self.transaction_executor = MockTransactionExecutor( batch_execution_result=None) self.executor = SynchronousExecutor() self.block_validator = BlockValidator( state_view_factory=self.state_view_factory, block_cache=self.block_tree_manager.block_cache, transaction_executor=self.transaction_executor, squash_handler=None, identity_signer=self.block_tree_manager.identity_signer, data_dir=None, config_dir=None, permission_verifier=self.permission_verifier, thread_pool=self.executor) def chain_updated(head, committed_batches=None, uncommitted_batches=None): pass self.chain_ctrl = ChainController( block_cache=self.block_tree_manager.block_cache, block_validator=self.block_validator, state_view_factory=self.state_view_factory, chain_head_lock=self._chain_head_lock, on_chain_updated=chain_updated, chain_id_manager=self.chain_id_manager, data_dir=None, config_dir=None, chain_observers=[]) init_root = self.chain_ctrl.chain_head self.assert_is_chain_head(init_root) # create a chain of length 5 extending the root _, head = self.generate_chain(init_root, 5) self.receive_and_process_blocks(head) self.assert_is_chain_head(head) self.init_head = head # next multi threaded # next add block publisher # next batch lists # integrate with LMDB # early vs late binding ( class member of consensus BlockPublisher) # helpers def assert_is_chain_head(self, block): chain_head_sig = self.chain_ctrl.chain_head.header_signature block_sig = block.header_signature # assert chain_head_sig[:8] == block_sig[:8] def generate_chain(self, root_block, num_blocks, params=None): '''Returns (chain, chain_head). Usually only the head is needed, but occasionally the chain itself is used. ''' if params is None: params = {'add_to_cache': True} chain = self.block_tree_manager.generate_chain(root_block, num_blocks, params) head = chain[-1] return chain, head def generate_block(self, *args, **kwargs): return self.block_tree_manager.generate_block(*args, **kwargs) def receive_and_process_blocks(self, *blocks): for block in blocks: self.chain_ctrl.on_block_received(block) self.executor.process_all()
class TestChainCommitState(unittest.TestCase): def setUp(self): self.commit_state = None self.block_tree_manager = BlockTreeManager() def test_fall_thru(self): """ Test that the requests correctly fall thru to the underlying BlockStore. """ blocks = self.generate_chain(1) commit_state = self.create_chain_commit_state(blocks) self.commit_state = commit_state self.assert_block_present(blocks[0]) self.assert_missing() def test_uncommitted_blocks(self): """ Test that the ChainCommitState can simulate blocks being uncommited from the BlockStore """ blocks = self.generate_chain(2) block, uncommitted_block = blocks commit_state = self.create_chain_commit_state( blocks=blocks, uncommitted_blocks=[uncommitted_block], ) self.commit_state = commit_state # the first block is still present self.assert_block_present(block) # batch from the uncommited block should not be present self.assert_block_not_present(uncommitted_block) self.assert_missing() def test_add_remove_batch(self): """ Test that we can incrementatlly build the commit state and roll it back """ blocks = self.generate_chain(2) block, uncommitted_block = blocks commit_state = self.create_chain_commit_state( blocks=blocks, uncommitted_blocks=[uncommitted_block], ) self.commit_state = commit_state # the first block is still present self.assert_block_present(block) # batch from the uncommited block should not be present self.assert_block_not_present(uncommitted_block) batch = uncommitted_block.batches[0] commit_state.add_batch(batch) # the batch should appear present self.assert_batch_present(batch) # check that we can remove the batch again. commit_state.remove_batch(batch) self.assert_batch_not_present(batch) # Do an incremental add of the batch for txn in batch.transactions: commit_state.add_txn(txn.header_signature) self.assert_txn_present(txn) self.assertFalse(commit_state.has_batch(batch.header_signature)) commit_state.add_batch(batch, add_transactions=False) self.assert_batch_present(batch) # check that we can remove the batch again. commit_state.remove_batch(batch) self.assert_batch_not_present(batch) def generate_chain(self, block_count): return self.block_tree_manager.generate_chain( None, [{} for _ in range(block_count)]) def create_chain_commit_state(self, blocks, uncommitted_blocks=None, chain_head=None): block_store = BlockStore(DictDatabase()) block_store.update_chain(blocks) if chain_head is None: chain_head = block_store.chain_head.identifier if uncommitted_blocks is None: uncommitted_blocks = [] return ChainCommitState(block_store, uncommitted_blocks) def assert_txn_present(self, txn): self.assertTrue(self.commit_state.has_transaction( txn.header_signature)) def assert_batch_present(self, batch): self.assertTrue(self.commit_state.has_batch(batch.header_signature)) for txn in batch.transactions: self.assert_txn_present(txn) def assert_block_present(self, block): for batch in block.batches: self.assert_batch_present(batch) def assert_txn_not_present(self, txn): self.assertFalse( self.commit_state.has_transaction(txn.header_signature)) def assert_batch_not_present(self, batch): self.assertFalse(self.commit_state.has_batch(batch.header_signature)) for txn in batch.transactions: self.assert_txn_not_present(txn) def assert_block_not_present(self, block): for batch in block.batches: self.assert_batch_not_present(batch) def assert_missing(self): """check missing keys behave as expected. """ self.assertFalse(self.commit_state.has_batch("missing")) self.assertFalse(self.commit_state.has_transaction("missing"))
class TestBlockValidator(unittest.TestCase): def setUp(self): self.btm = BlockTreeManager() def create_block_validator(self, new_block, on_block_validated): return BlockValidator(consensus=TestModeVerifier(), new_block=new_block, chain_head=self.btm.chain_head, block_cache=self.btm.block_cache, done_cb=on_block_validated, executor=MockTransactionExecutor(), squash_handler=None) class BlockValidationHandler(object): def __init__(self): self.result = None def on_block_validated(self, commit_new_block, result): result["commit_new_block"] = commit_new_block self.result = result def has_result(self): return self.result is not None # fork based tests def test_fork_simple(self): """ Test a simple case of a new block extending the current root. """ bvh = self.BlockValidationHandler() new_block = self.btm.generate_block(previous_block=self.btm.chain_head, add_to_store=True) bv = self.create_block_validator(new_block, bvh.on_block_validated) bv.run() self.assertTrue(bvh.has_result()) self.assertTrue(new_block.status == BlockStatus.Valid) self.assertTrue(bvh.result["commit_new_block"]) def test_good_fork_lower(self): """ Test case of a new block extending on a valid chain but not as long as the current chain. """ bvh = self.BlockValidationHandler() root = self.btm.chain_head # create a new valid chain 5 long from the current root new_head = self.btm.generate_chain(root, 5, {'add_to_store': True}) self.btm.set_chain_head(new_head[-1]) # generate candidate chain 3 long from the same root new_block = self.btm.generate_chain(root, 3, {'add_to_cache': True}) bv = self.create_block_validator(new_block[-1], bvh.on_block_validated) bv.run() self.assertTrue(bvh.has_result()) self.assertTrue(new_block[-1].status == BlockStatus.Valid) self.assertFalse(bvh.result["commit_new_block"]) def test_good_fork_higher(self): """ Test case of a new block extending on a valid chain but longer than the current chain. ( similar to test_good_fork_lower but uses a different code path when finding the common root ) """ bvh = self.BlockValidationHandler() root = self.btm.chain_head # create a new valid chain 5 long from the current root new_head = self.btm.generate_chain(root, 5, {'add_to_store': True}) self.btm.set_chain_head(new_head[-1]) # generate candidate chain 8 long from the same root new_block = self.btm.generate_chain(root, 8, {'add_to_cache': True}) bv = self.create_block_validator(new_block[-1], bvh.on_block_validated) bv.run() self.assertTrue(bvh.has_result()) self.assertTrue(new_block[-1].status == BlockStatus.Valid) self.assertTrue(bvh.result["commit_new_block"]) def test_fork_different_genesis(self): """" Test the case where new block is from a different genesis """ bvh = self.BlockValidationHandler() # create a new valid chain 5 long from the current root new_head = self.btm.generate_chain(self.btm.chain_head, 5, {'add_to_store': True}) self.btm.set_chain_head(new_head[-1]) # generate candidate chain 5 long from it's own genesis new_block = self.btm.generate_chain(None, 5, {'add_to_cache': True}) bv = self.create_block_validator(new_block[-1], bvh.on_block_validated) bv.run() self.assertTrue(bvh.has_result()) self.assertTrue(new_block[-1].status == BlockStatus.Invalid) self.assertFalse(bvh.result["commit_new_block"]) def test_fork_missing_predecessor(self): """" Test the case where new block is missing the a predecessor """ bvh = self.BlockValidationHandler() root = self.btm.chain_head # generate candidate chain 3 long off the current head. new_block = self.btm.generate_chain(root, 3, {'add_to_cache': True}) # remove one of the new blocks del self.btm.block_cache[new_block[1].identifier] bv = self.create_block_validator(new_block[-1], bvh.on_block_validated) bv.run() self.assertTrue(bvh.has_result()) self.assertTrue(new_block[-1].status == BlockStatus.Invalid) self.assertFalse(bvh.result["commit_new_block"]) def test_fork_invalid_predecessor(self): """" Test the case where new block has an invalid predecessor """ bvh = self.BlockValidationHandler() root = self.btm.chain_head # generate candidate chain 3 long off the current head. new_block = self.btm.generate_chain(root, 3, {'add_to_cache': True}) # Mark a predecessor as invalid new_block[1].status = BlockStatus.Invalid bv = self.create_block_validator(new_block[-1], bvh.on_block_validated) bv.run() self.assertTrue(bvh.has_result()) self.assertTrue(new_block[-1].status == BlockStatus.Invalid) self.assertFalse(bvh.result["commit_new_block"]) # block based tests def test_block_bad_signature(self): """ Test the case where the new block has a bad signature. """ pass def test_block_missing_batch(self): """ Test the case where the new block is missing a batch. """ pass def test_block_extra_batch(self): """ Test the case where the new block has a batch. """ pass def test_block_batches_order(self): """ Test the case where the new block has batches that are out of order. """ pass def test_block_bad_batch(self): """ Test the case where the new block has a bad batch """ pass def test_block_missing_batch_dependency(self): """ Test the case where the new block has a batch that is missing a dependency. """ pass def test_block_bad_state(self): """ Test the case where the new block has a bad batch """ pass def test_block_bad_consensus(self): """ Test the case where the new block has a bad batch """ pass
class TestChainController(unittest.TestCase): def setUp(self): self.blocks = BlockTreeManager() self.gossip = MockNetwork() self.executor = SynchronousExecutor() self.txn_executor = MockTransactionExecutor() def chain_updated(head): pass self.chain_ctrl = ChainController( consensus=TestModeVerifier(), block_store=self.blocks.block_store, send_message=self.gossip.send_message, executor=self.executor, transaction_executor=MockTransactionExecutor(), on_chain_updated=chain_updated, squash_handler=None) def test_simple_case(self): # TEST Run the simple case block_1 = self.blocks.generate_block(self.blocks.chain_head) self.chain_ctrl.on_block_received(block_1.get_block()) self.executor.process_all() assert (self.chain_ctrl.chain_head.block.header_signature == block_1.header_signature) def test_alternate_genesis(self): # TEST Run generate and alternate genesis block head = self.chain_ctrl.chain_head other_genesis = self.blocks.generate_block(add_to_store=True) for b in self.blocks.generate_chain(other_genesis, 5): self.chain_ctrl.on_block_received(b.get_block()) self.executor.process_all() assert (self.chain_ctrl.chain_head.block.header_signature == head.block.header_signature) def test_bad_block_signature(self): # TEST Bad block extending current chain # Bad due to signature head = self.blocks.chain_head block_bad = self.blocks.generate_block(self.blocks.chain_head.block, invalid_signature=True) self.chain_ctrl.on_block_received(block_bad.get_block()) assert (self.chain_ctrl.chain_head.block.header_signature == head.block.header_signature) def test_bad_block_consensus(self): # Bad due to consensus pass def test_bad_block_transaction(self): # Bad due to transaction pass def test_missing_block(self): # TEST Missing block G->missing->B head = self.blocks.chain_head new_blocks = self.blocks.generate_chain(head, 2) self.chain_ctrl.on_block_received(new_blocks[1].get_block()) self.executor.process_all() assert (len(self.gossip.messages) == 1) block_id = self.gossip.messages[0] assert (block_id == new_blocks[0].header_signature) self.gossip.clear() self.chain_ctrl.on_block_received(new_blocks[0].get_block()) self.executor.process_all() assert (self.chain_ctrl.chain_head.block.header_signature == new_blocks[1].header_signature) def test_missing_block_invalid_head(self): # TEST Missing block G->missing->B # B is invalid but Missing is valid head = self.blocks.chain_head new_blocks_def = self.blocks.generate_chain_definition(2) new_blocks_def[1]["invalid_signature"] = True new_blocks = self.blocks.generate_chain(head, new_blocks_def) self.chain_ctrl.on_block_received(new_blocks[1].get_block()) self.executor.process_all() assert (len(self.gossip.messages) == 1) block_id = self.gossip.messages[0] assert (block_id == new_blocks[0].header_signature) self.gossip.clear() self.chain_ctrl.on_block_received(new_blocks[0].get_block()) self.executor.process_all() pp.pprint(new_blocks) pp.pprint(self.blocks.block_store)
class TestChainController(unittest.TestCase): def setUp(self): self.block_tree_manager = BlockTreeManager() self.gossip = MockNetwork() self.executor = SynchronousExecutor() self.txn_executor = MockTransactionExecutor() self.block_sender = MockBlockSender() self.chain_id_manager = MockChainIdManager() self._chain_head_lock = RLock() self.state_delta_processor = MockStateDeltaProcessor() def chain_updated(head, committed_batches=None, uncommitted_batches=None): pass self.chain_ctrl = ChainController( block_cache=self.block_tree_manager.block_cache, state_view_factory=MockStateViewFactory( self.block_tree_manager.state_db), block_sender=self.block_sender, executor=self.executor, transaction_executor=MockTransactionExecutor( batch_execution_result=None), chain_head_lock=self._chain_head_lock, on_chain_updated=chain_updated, squash_handler=None, chain_id_manager=self.chain_id_manager, state_delta_processor=self.state_delta_processor, identity_signing_key=self.block_tree_manager.identity_signing_key, data_dir=None, config_dir=None) init_root = self.chain_ctrl.chain_head self.assert_is_chain_head(init_root) # create a chain of length 5 extending the root _, head = self.generate_chain(init_root, 5) self.receive_and_process_blocks(head) self.assert_is_chain_head(head) self.init_head = head def test_simple_case(self): new_block = self.generate_block(self.init_head) self.receive_and_process_blocks(new_block) self.assert_is_chain_head(new_block) # validate that the deltas for the new block are published self.assertEqual(new_block, self.state_delta_processor.block) def test_alternate_genesis(self): '''Tests a fork extending an alternate genesis block ''' chain, head = self.generate_chain(None, 5) for block in chain: self.receive_and_process_blocks(block) # make sure initial head is still chain head self.assert_is_chain_head(self.init_head) def test_bad_blocks(self): '''Tests bad blocks extending current chain ''' # Bad due to consensus bad_consen = self.generate_block( previous_block=self.init_head, invalid_consensus=True) # chain head should be the same self.receive_and_process_blocks(bad_consen) self.assert_is_chain_head(self.init_head) # Bad due to transaction bad_batch = self.generate_block( previous_block=self.init_head, invalid_batch=True) # chain head should be the same self.receive_and_process_blocks(bad_batch) self.assert_is_chain_head(self.init_head) # # Ensure good block works good_block = self.generate_block( previous_block=self.init_head) # chain head should be good_block self.receive_and_process_blocks(good_block) self.assert_is_chain_head(good_block) def test_fork_weights(self): '''Tests extending blocks of different weights ''' weight_4 = self.generate_block( previous_block=self.init_head, weight=4) weight_7 = self.generate_block( previous_block=self.init_head, weight=7) weight_8 = self.generate_block( previous_block=self.init_head, weight=8) self.receive_and_process_blocks( weight_7, weight_4, weight_8) self.assert_is_chain_head(weight_8) def test_fork_lengths(self): '''Tests competing forks of different lengths ''' _, head_2 = self.generate_chain(self.init_head, 2) _, head_7 = self.generate_chain(self.init_head, 7) _, head_5 = self.generate_chain(self.init_head, 5) self.receive_and_process_blocks( head_2, head_7, head_5) self.assert_is_chain_head(head_7) def test_advancing_chain(self): '''Tests the chain being advanced between a fork's creation and validation ''' _, fork_5 = self.generate_chain(self.init_head, 5) _, fork_3 = self.generate_chain(self.init_head, 3) self.receive_and_process_blocks(fork_3) self.assert_is_chain_head(fork_3) # fork_5 is longer than fork_3, so it should be accepted self.receive_and_process_blocks(fork_5) self.assert_is_chain_head(fork_5) def test_fork_missing_block(self): '''Tests a fork with a missing block ''' # make new chain new_chain, new_head = self.generate_chain(self.init_head, 5) self.chain_ctrl.on_block_received(new_head) # delete a block from the new chain del self.chain_ctrl._block_cache[new_chain[3].identifier] self.executor.process_all() # chain shouldn't advance self.assert_is_chain_head(self.init_head) # try again, chain still shouldn't advance self.receive_and_process_blocks(new_head) self.assert_is_chain_head(self.init_head) def test_fork_bad_block(self): '''Tests a fork with a bad block in the middle ''' # make two chains extending chain good_chain, good_head = self.generate_chain(self.init_head, 5) bad_chain, bad_head = self.generate_chain(self.init_head, 5) self.chain_ctrl.on_block_received(bad_head) self.chain_ctrl.on_block_received(good_head) # invalidate block in the middle of bad_chain bad_chain[3].status = BlockStatus.Invalid self.executor.process_all() # good_chain should be accepted self.assert_is_chain_head(good_head) def test_advancing_fork(self): '''Tests a fork advancing before getting validated ''' _, fork_head = self.generate_chain(self.init_head, 5) self.chain_ctrl.on_block_received(fork_head) # advance fork before it gets accepted _, ext_head = self.generate_chain(fork_head, 3) self.executor.process_all() self.assert_is_chain_head(fork_head) self.receive_and_process_blocks(ext_head) self.assert_is_chain_head(ext_head) def test_block_extends_in_validation(self): '''Tests a block getting extended while being validated ''' # create candidate block candidate = self.block_tree_manager.generate_block( previous_block=self.init_head) self.assert_is_chain_head(self.init_head) # queue up the candidate block, but don't process self.chain_ctrl.on_block_received(candidate) # create a new block extending the candidate block extending_block = self.block_tree_manager.generate_block( previous_block=candidate) self.assert_is_chain_head(self.init_head) # queue and process the extending block, # which should be the new head self.receive_and_process_blocks(extending_block) self.assert_is_chain_head(extending_block) def test_multiple_extended_forks(self): '''A more involved example of competing forks Three forks of varying lengths a_0, b_0, and c_0 are created extending the existing chain, with c_0 being the longest initially. The chains are extended in the following sequence: 1. Extend all forks by 2. The c fork should remain the head. 2. Extend forks by lenths such that the b fork is the longest. It should be the new head. 3. Extend all forks by 8. The b fork should remain the head. 4. Create a new fork of the initial chain longer than any of the other forks. It should be the new head. ''' # create forks of various lengths _, a_0 = self.generate_chain(self.init_head, 3) _, b_0 = self.generate_chain(self.init_head, 5) _, c_0 = self.generate_chain(self.init_head, 7) self.receive_and_process_blocks(a_0, b_0, c_0) self.assert_is_chain_head(c_0) # extend every fork by 2 _, a_1 = self.generate_chain(a_0, 2) _, b_1 = self.generate_chain(b_0, 2) _, c_1 = self.generate_chain(c_0, 2) self.receive_and_process_blocks(a_1, b_1, c_1) self.assert_is_chain_head(c_1) # extend the forks by different lengths _, a_2 = self.generate_chain(a_1, 1) _, b_2 = self.generate_chain(b_1, 6) _, c_2 = self.generate_chain(c_1, 3) self.receive_and_process_blocks(a_2, b_2, c_2) self.assert_is_chain_head(b_2) # extend every fork by 2 _, a_3 = self.generate_chain(a_2, 8) _, b_3 = self.generate_chain(b_2, 8) _, c_3 = self.generate_chain(c_2, 8) self.receive_and_process_blocks(a_3, b_3, c_3) self.assert_is_chain_head(b_3) # create a new longest chain _, wow = self.generate_chain(self.init_head, 30) self.receive_and_process_blocks(wow) self.assert_is_chain_head(wow) # next multi threaded # next add block publisher # next batch lists # integrate with LMDB # early vs late binding ( class member of consensus BlockPublisher) # helpers def assert_is_chain_head(self, block): chain_head_sig = self.chain_ctrl.chain_head.header_signature block_sig = block.header_signature self.assertEqual( chain_head_sig[:8], block_sig[:8], 'Not chain head') def generate_chain(self, root_block, num_blocks, params={'add_to_cache': True}): '''Returns (chain, chain_head). Usually only the head is needed, but occasionally the chain itself is used. ''' chain = self.block_tree_manager.generate_chain( root_block, num_blocks, params) head = chain[-1] return chain, head def generate_block(self, *args, **kwargs): return self.block_tree_manager.generate_block( *args, **kwargs) def receive_and_process_blocks(self, *blocks): for block in blocks: self.chain_ctrl.on_block_received(block) self.executor.process_all()
class TestBlockValidator(unittest.TestCase): def setUp(self): self.state_view_factory = MockStateViewFactory() self.block_tree_manager = BlockTreeManager() self.root = self.block_tree_manager.chain_head self.block_validation_handler = self.BlockValidationHandler() # fork based tests def test_fork_simple(self): """ Test a simple case of a new block extending the current root. """ new_block = self.block_tree_manager.generate_block( previous_block=self.root) self.validate_block(new_block) self.assert_valid_block(new_block) self.assert_new_block_committed() def test_good_fork_lower(self): """ Test case of a new block extending on a valid chain but not as long as the current chain. """ # create a new valid chain 5 long from the current root chain, head = self.generate_chain_with_head( self.root, 5, {'add_to_store': True}) self.block_tree_manager.set_chain_head(head) # generate candidate chain 3 long from the same root new_chain, new_head = self.generate_chain_with_head( self.root, 3, {'add_to_cache': True}) self.validate_block(new_head) self.assert_valid_block(new_head) self.assert_new_block_not_committed() def test_good_fork_higher(self): """ Test case of a new block extending on a valid chain but longer than the current chain. ( similar to test_good_fork_lower but uses a different code path when finding the common root ) """ # create a new valid chain 5 long from the current root chain, head = self.generate_chain_with_head( self.root, 5, {'add_to_store': True}) self.block_tree_manager.set_chain_head(head) # generate candidate chain 8 long from the same root new_chain, new_head = self.generate_chain_with_head( head, 8, {'add_to_cache': True}) self.validate_block(new_head) self.assert_valid_block(new_head) self.assert_new_block_committed() def test_fork_different_genesis(self): """" Test the case where new block is from a different genesis """ # create a new valid chain 5 long from the current root chain, head = self.generate_chain_with_head( self.root, 5, {'add_to_store': True}) self.block_tree_manager.set_chain_head(head) # generate candidate chain 5 long from its own genesis new_chain, new_head = self.generate_chain_with_head( None, 5, {'add_to_cache': True}) self.validate_block(new_head) self.assert_invalid_block(new_head) self.assert_new_block_not_committed() def test_fork_missing_predecessor(self): """" Test the case where new block is missing the a predecessor """ # generate candidate chain 5 long off the current head. chain, head = self.generate_chain_with_head( self.root, 5, {'add_to_cache': True}) # remove one of the new blocks del self.block_tree_manager.block_cache[chain[1].identifier] self.validate_block(head) self.assert_invalid_block(head) self.assert_new_block_not_committed() def test_fork_invalid_predecessor(self): """" Test the case where new block has an invalid predecessor """ # generate candidate chain 5 long off the current head. chain, head = self.generate_chain_with_head( self.root, 5, {'add_to_cache': True}) # Mark a predecessor as invalid chain[1].status = BlockStatus.Invalid self.validate_block(head) self.assert_invalid_block(head) self.assert_new_block_not_committed() def test_block_bad_consensus(self): """ Test the case where the new block has a bad batch """ chain, head = self.generate_chain_with_head( self.root, 5, {'add_to_store': True}) new_block = self.block_tree_manager.generate_block( previous_block=head, add_to_cache=True, invalid_consensus=True) self.validate_block(new_block) self.assert_invalid_block(new_block) self.assert_new_block_not_committed() def test_block_bad_batch(self): """ Test the case where the new block has a bad batch """ chain, head = self.generate_chain_with_head( self.root, 5, {'add_to_store': True}) new_block = self.block_tree_manager.generate_block( previous_block=head, add_to_cache=True, invalid_batch=True) self.validate_block(new_block) self.assert_invalid_block(new_block) self.assert_new_block_not_committed() def test_block_missing_batch_dependency(self): """ Test the case where the new block has a batch that is missing a dependency. """ pass # assertions def assert_valid_block(self, block): self.assertEqual( block.status, BlockStatus.Valid, "Block should be valid") def assert_invalid_block(self, block): self.assertEqual( block.status, BlockStatus.Invalid, "Block should be invalid") def assert_new_block_committed(self): self.assert_handler_has_result() self.assertTrue( self.block_validation_handler.result["commit_new_block"], "New block not committed, should be") def assert_new_block_not_committed(self): self.assert_handler_has_result() self.assertFalse( self.block_validation_handler.result["commit_new_block"], "New block committed, shouldn't be") def assert_handler_has_result(self): msg = "Validation handler doesn't have result" self.assertTrue(self.block_validation_handler.has_result(), msg) # block validation def validate_block(self, block): validator = self.create_block_validator( block, self.block_validation_handler.on_block_validated) validator.run() def create_block_validator(self, new_block, on_block_validated): return BlockValidator( consensus_module=mock_consensus, new_block=new_block, chain_head=self.block_tree_manager.chain_head, state_view_factory=self.state_view_factory, block_cache=self.block_tree_manager.block_cache, done_cb=on_block_validated, executor=MockTransactionExecutor(batch_execution_result=None), squash_handler=None, identity_signing_key=self.block_tree_manager.identity_signing_key, data_dir=None, config_dir=None) class BlockValidationHandler(object): def __init__(self): self.result = None def on_block_validated(self, commit_new_block, result): result["commit_new_block"] = commit_new_block self.result = result def has_result(self): return self.result is not None # block tree manager interface def generate_chain_with_head(self, root_block, num_blocks, params=None): chain = self.block_tree_manager.generate_chain( root_block, num_blocks, params) head = chain[-1] return chain, head