diff --git a/carnot/test_unhappy_path.py b/carnot/test_unhappy_path.py index 45a2052..428117a 100644 --- a/carnot/test_unhappy_path.py +++ b/carnot/test_unhappy_path.py @@ -1,30 +1,8 @@ from carnot import * from unittest import TestCase +from itertools import chain -# Unhappy path tests - -# 1: At the end of the timeout the highQC in the next leader's aggregatedQC should be the highestQC held by the -# majority of nodes or a qc higher than th highestQC held by the majority of nodes. -# Majority means more than two thirds of total number of nodes, randomly assigned to committees. - - -# 2: Have consecutive view changes and verify the following state variable: -# last_timeout_view_qc.view -# high_qc.view -# current_view -# last_voted_view - -# 3: Due failure consecutive condition between parent and grandparent blocks might not meet. So whenever the -# Consecutive view condition in the try_to_commit fails, then all the blocks between the latest_committed_block and the -# grandparent (including the grandparent) must be committed in order. -# As far as I know current code only executes the grandparent only. It should also address the case above. - - -# 4: Have consecutive success adding two blocks then a failure and two consecutive success + 1 failure+ 1 success -# S1 <- S2 <- F1 <- S3 <- S4 <-F2 <- S5 -# At S3, S1 should be committed. At S5, S2 and S3 must be committed - class MockCarnot(Carnot): def __init__(self, id): super(MockCarnot, self).__init__(id) @@ -40,17 +18,196 @@ class MockCarnot(Carnot): pass -class TestCarnotHappyPath(TestCase): - @staticmethod - def add_genesis_block(carnot: Carnot) -> Block: - genesis_block = Block(view=0, qc=StandardQc(block=b"", view=0), _id=b"") - carnot.safe_blocks[genesis_block.id()] = genesis_block - carnot.receive_block(genesis_block) - carnot.increment_voted_view(0) - carnot.local_high_qc = genesis_block.qc - carnot.current_view = 1 - carnot.committed_blocks[genesis_block.id()] = genesis_block - return genesis_block +class MockOverlay(Overlay): + """ + Overlay for 5 nodes where the leader is the single member of the root committee + 0 + │ + 1◄──┴──►2 + │ + 3◄─┴─►4 + """ + + def __init__(self): + self.parents = { + int_to_id(1): {int_to_id(0)}, + int_to_id(2): {int_to_id(0)}, + int_to_id(3): {int_to_id(1)}, + int_to_id(4): {int_to_id(1)} + } + + self.childs = { + int_to_id(0): { + int_to_id(1), int_to_id(2) + }, + int_to_id(1): { + int_to_id(3), int_to_id(4) + } + } + + self.leafs = { + int_to_id(2), int_to_id(3), int_to_id(4) + } + + def leaf_committees(self) -> Set[Committee]: + return [[leaf] for leaf in self.leafs] + + def root_committee(self) -> Committee: + return {int_to_id(0)} + + def is_child_of_root_committee(self, _id: Id) -> bool: + return _id in {int_to_id(1), int_to_id(2)} + + def is_member_of_child_committee(self, parent: Id, child: Id) -> bool: + return child in childs if (childs := self.childs.get(parent)) else False + + def leader_super_majority_threshold(self, _id: Id) -> int: + return 3 + + def is_leader(self, _id: Id): + # Leader is the node with id 0, otherwise not + return _id == int_to_id(0) + + def is_member_of_root_committee(self, _id: Id): + return _id == int_to_id(0) + + def leader(self, view: View) -> Id: + return int_to_id(0) + + def parent_committee(self, _id: Id) -> Optional[Committee]: + return self.parents.get(_id) + + def is_member_of_leaf_committee(self, _id: Id) -> bool: + return _id in self.leafs + + def super_majority_threshold(self, _id: Id) -> int: + thresholds = { + int_to_id(0): 2, + int_to_id(1): 2, + } + return thresholds.get(_id, 0) + + +def add_genesis_block(carnot: Carnot) -> Block: + genesis_block = Block(view=0, qc=StandardQc(block=b"", view=0), _id=b"") + carnot.safe_blocks[genesis_block.id()] = genesis_block + carnot.receive_block(genesis_block) + carnot.increment_voted_view(0) + carnot.local_high_qc = genesis_block.qc + carnot.current_view = 1 + carnot.committed_blocks[genesis_block.id()] = genesis_block + return genesis_block + + +def setup_initial_setup(test_case: TestCase, overlay: MockOverlay, size: int) -> (Dict[Id, Carnot], MockCarnot, Block): + nodes = {int_to_id(i): MockCarnot(int_to_id(i)) for i in range(size)} + # add overlay + for node in nodes.values(): + node.overlay = overlay + leader: MockCarnot = nodes[overlay.leader(0)] + genesis_block = None + for node in nodes.values(): + genesis_block = add_genesis_block(node) + # votes for genesis block + genesis_votes = set( + Vote( + block=genesis_block.id(), + view=0, + voter=int_to_id(i), + qc=StandardQc( + block=genesis_block.id(), + view=0 + ), + ) for i in range(5) + ) + leader.propose_block(1, genesis_votes) + proposed_block = leader.latest_event + test_case.assertIsNotNone(proposed_block) + return nodes, leader, proposed_block + + +def parents_from_childs(overlay: MockOverlay, childs: List[Id]) -> Set[Id]: + if len(childs) == 0: + return set() + possible_parents = filter( + lambda x: x is not None, + chain.from_iterable(parent for _id in childs if (parent := overlay.parent_committee(_id))) + ) + return set(possible_parents) if possible_parents else set() + + +def succeed(test_case: TestCase, overlay: MockOverlay, nodes: Dict[Id, MockCarnot], proposed_block: Block) -> List[Vote]: + # broadcast the block + for node in nodes.values(): + node.receive_block(proposed_block) + + votes = {} + childs_ids = list(chain.from_iterable(overlay.leaf_committees())) + leafs = [nodes[_id] for _id in childs_ids] + for node in leafs: + node.approve_block(proposed_block, set()) + votes[node.id] = node.latest_event + + while len(parents := parents_from_childs(overlay, childs_ids)) != 0: + for node_id in parents: + node = nodes[node_id] + child_votes = [votes[_id] for _id in votes.keys() if overlay.is_member_of_child_committee(node_id, _id)] + if len(child_votes) == overlay.super_majority_threshold(node_id) and node_id not in votes: + node.approve_block(proposed_block, child_votes) + votes[node_id] = node.latest_event + childs_ids = list(set(parents)) + + root_votes = [ + nodes[node_id].latest_event + for node_id in nodes + if overlay.is_member_of_root_committee(node_id) or overlay.is_child_of_root_committee(node_id) + ] + return root_votes + + +def fail(test_case: TestCase, overlay: MockOverlay, nodes: Dict[Id, MockCarnot], proposed_block: Block) -> List[NewView]: + # broadcast the block + for node in nodes.values(): + node.receive_block(proposed_block) + + node: MockCarnot + timeouts = [] + for node in (nodes[_id] for _id in nodes if overlay.is_member_of_root_committee(_id) or overlay.is_child_of_root_committee(_id)): + node.local_timeout() + timeouts.append(node.latest_event) + + root_member = next(nodes[_id] for _id in nodes if overlay.is_member_of_root_committee(_id)) + root_member.timeout_detected(timeouts) + timeout_qc = root_member.latest_event + + for node in nodes.values(): + node.received_timeout_qc(timeout_qc) + + votes = {} + childs_ids = list(chain.from_iterable(overlay.leaf_committees())) + leafs = [nodes[_id] for _id in childs_ids] + for node in leafs: + node.approve_new_view(timeout_qc, set()) + votes[node.id] = node.latest_event + + while len(parents := parents_from_childs(overlay, childs_ids)) != 0: + for node_id in parents: + node = nodes[node_id] + child_votes = [votes[_id] for _id in votes.keys() if overlay.is_member_of_child_committee(node_id, _id)] + if len(child_votes) == overlay.super_majority_threshold(node_id) and node_id not in votes: + node.approve_new_view(timeout_qc, child_votes) + votes[node_id] = node.latest_event + childs_ids = list(set(parents)) + + root_votes = [ + nodes[node_id].latest_event + for node_id in nodes + if overlay.is_member_of_root_committee(node_id) or overlay.is_child_of_root_committee(node_id) + ] + return root_votes + + +class TestCarnotUnhappyPath(TestCase): def test_timeout_high_qc(self): """ @@ -59,99 +216,9 @@ class TestCarnotHappyPath(TestCase): Majority means more than two thirds of total number of nodes, randomly assigned to committees. """ - class MockOverlay(Overlay): - """ - Overlay for 5 nodes where the leader is the single member of the root committee - 0 - │ - 1◄──┴──►2 - │ - 3◄─┴─►4 - """ - - def __init__(self): - self.parents = { - int_to_id(1): int_to_id(0), - int_to_id(2): int_to_id(0), - int_to_id(3): int_to_id(1), - int_to_id(4): int_to_id(1) - } - - self.childs = { - int_to_id(0): { - int_to_id(1), int_to_id(2) - }, - int_to_id(1): { - int_to_id(3), int_to_id(4) - } - } - - self.leafs = { - int_to_id(2), int_to_id(3), int_to_id(4) - } - - def leaf_committees(self) -> Set[Committee]: - return {set(leaf) for leaf in self.leafs} - - def root_committee(self) -> Committee: - return {int_to_id(0)} - - def is_child_of_root_committee(self, _id: Id) -> bool: - return _id in {int_to_id(1), int_to_id(2)} - - def is_member_of_child_committee(self, parent: Id, child: Id) -> bool: - return child in childs if (childs := self.childs.get(parent)) else False - - def leader_super_majority_threshold(self, _id: Id) -> int: - return 3 - - def is_leader(self, _id: Id): - # Leader is the node with id 0, otherwise not - return _id == int_to_id(0) - - def is_member_of_root_committee(self, _id: Id): - return _id == int_to_id(0) - - def leader(self, view: View) -> Id: - return int_to_id(0) - - def parent_committee(self, _id: Id) -> Optional[Committee]: - return self.parents.get(_id) - - def is_member_of_leaf_committee(self, _id: Id) -> bool: - return _id in self.leafs - - def super_majority_threshold(self, _id: Id) -> int: - thresholds = { - int_to_id(0): 2, - int_to_id(1): 2, - } - return thresholds.get(_id, 0) - - nodes = {int_to_id(i): MockCarnot(int_to_id(i)) for i in range(5)} overlay = MockOverlay() - # add overlay - for node in nodes.values(): - node.overlay = overlay - leader: MockCarnot = nodes[int_to_id(0)] - genesis_block = None - for node in nodes.values(): - genesis_block = self.add_genesis_block(node) - # votes for genesis block - genesis_votes = set( - Vote( - block=genesis_block.id(), - view=0, - voter=int_to_id(i), - qc=StandardQc( - block=genesis_block.id(), - view=0 - ), - ) for i in range(5) - ) - leader.propose_block(1, genesis_votes) - proposed_block = leader.latest_event - self.assertIsNotNone(proposed_block) + + nodes, leader, proposed_block = setup_initial_setup(self, overlay, 5) for view in range(1, 4): node: MockCarnot @@ -199,3 +266,36 @@ class TestCarnotHappyPath(TestCase): node.receive_block(proposed_block) for node in nodes.values(): self.assertEqual(node.latest_committed_view, 0) + + def test_interleave_success_fails(self): + """ + At the end of the timeout the highQC in the next leader's aggregatedQC should be the highestQC held by the + majority of nodes or a qc higher than th highestQC held by the majority of nodes. + Majority means more than two thirds of total number of nodes, randomly assigned to committees. + """ + overlay = MockOverlay() + leader: MockCarnot + nodes, leader, proposed_block = setup_initial_setup(self, overlay, 5) + + for view in range(2, 5): + root_votes = succeed(self, overlay, nodes, proposed_block) + leader.propose_block(view, root_votes) + proposed_block = leader.latest_event + + root_votes = fail(self, overlay, nodes, proposed_block) + leader.propose_block(5, root_votes) + proposed_block = leader.latest_event + + for view in range(6, 8): + root_votes = succeed(self, overlay, nodes, proposed_block) + leader.propose_block(view, root_votes) + proposed_block = leader.latest_event + + root_votes = fail(self, overlay, nodes, proposed_block) + leader.propose_block(8, root_votes) + proposed_block = leader.latest_event + + for view in range(9, 15): + root_votes = succeed(self, overlay, nodes, proposed_block) + leader.propose_block(view, root_votes) + proposed_block = leader.latest_event