diff --git a/carnot/carnot.py b/carnot/carnot.py index 6184f93..6999441 100644 --- a/carnot/carnot.py +++ b/carnot/carnot.py @@ -34,6 +34,7 @@ class AggregateQc: Qc: TypeAlias = StandardQc | AggregateQc + @dataclass class Block: view: View @@ -64,16 +65,32 @@ class Vote: @dataclass class TimeoutQc: view: View - high_qc: AggregateQc + high_qc: Qc + qc_views: List[View] + sender_ids: Set[Id] + sender: Id -Quorum: TypeAlias = Set[Vote] | Set[TimeoutQc] +# local timeout field is only used by the root committee and its children when they timeout. The timeout_qc is built +# from local_timeouts. Leaf nodes when receive timeout_qc build their timeout msg and includes the timeout_qc in it. +# The timeout_qc is indicator that the root committee and its child committees (if exist) have failed to collect votes. +@dataclass +class Timeout: + view: View + high_qc: Qc + sender: Id + timeout_qc: TimeoutQc + local_timeout: bool + + +Quorum: TypeAlias = Set[Vote] | Set[Timeout] class Overlay: """ Overlay structure for a View """ + @abstractmethod def is_leader(self, _id: Id): """ @@ -82,6 +99,20 @@ class Overlay: """ pass + def is_child_of_root(self, _id: Id): + """ + :param _id: Node id to be checked + :return: true if node is the member of child of the root committee + """ + pass + + def number_of_committees(self, _ids: set[Id]) -> int: + """ + :param _ids: Set of Node id to be checked + :return: Number of committees in the overlay + """ + pass + def leader(self, view: View) -> Id: """ :param view: @@ -117,7 +148,7 @@ class Overlay: def member_of_internal_com(self, _id: Id) -> bool: """ :param _id: - :return: truee if the participant with Id _id is member of internal committees within the committee tree overlay + :return: True if the participant with Id _id is member of internal committees within the committee tree overlay """ pass @@ -139,6 +170,18 @@ class Overlay: """ pass + def root_committee(self) -> Committee: + """ + :return: returns root committee + """ + pass + + def child_of_root_committee(self) -> Optional[Set[Committee]]: + """ + :return: returns child committee/s of root committee if present + """ + pass + @abstractmethod def super_majority_threshold(self, _id: Id) -> int: """ @@ -158,6 +201,10 @@ def download(view) -> Block: raise NotImplementedError +def build_timeout_qc(msgs) -> TimeoutQc: + pass + + class Carnot: def __init__(self, _id: Id): self.id: Id = _id @@ -182,6 +229,7 @@ class Carnot: return False return block.view >= self.current_view and block.view == (aggregated.view + 1) + # Ask Dani def update_high_qc(self, qc: Qc): match (self.local_high_qc, qc): case (None, StandardQc() as new_qc): @@ -193,6 +241,13 @@ class Carnot: case (old_qc, AggregateQc() as new_qc) if new_qc.high_qc().view != old_qc.view: self.local_high_qc = new_qc.high_qc() + def update_timeout_qc(self, timeout_qc: TimeoutQc): + match (self.last_timeout_view_qc, timeout_qc): + case (None, timeout_qc): + self.local_high_qc = timeout_qc + case (self.last_timeout_view_qc, timeout_qc) if timeout_qc.view > self.last_timeout_view_qc.view: + self.last_timeout_view_qc = timeout_qc + def receive_block(self, block: Block): assert block.parent() in self.safe_blocks if block.id() in self.safe_blocks or block.view <= self.latest_committed_view: @@ -248,16 +303,38 @@ class Carnot: self.broadcast(block) def local_timeout(self, new_overlay: Overlay): - self.last_timeout_view = self.current_view self.increment_voted_view(self.current_view) - self.overlay = new_overlay - if self.overlay.member_of_leaf_committee(self.id): - raise NotImplementedError() + if self.overlay.member_of_leaf_committee(self.id) or self.overlay.is_child_of_root(self.id): + timeout_msg: Timeout = Timeout( + view=self.current_view, + high_qc=self.local_high_qc, + local_timeout=True, + # local_timeout is only true for the root committee or members of its children + # root committee or its children can trigger the timeout. + timeout_qc=self.last_timeout_view_qc, + sender=self.id + ) + self.send(timeout_msg, *self.overlay.root_committee()) + for child_committee in self.overlay.child_of_root_committee(): + self.send(timeout_msg, child_committee) - def timeout(self, view: View, msgs: Set["TimeoutMsg"]): - raise NotImplementedError() + def timeout(self, msgs: Set["Timeout"]): + assert len(msgs) == self.overlay.super_majority_threshold(self.id) + assert all(msg.view == msgs.pop().view for msg in msgs) + assert msgs.pop().view > self.current_view + max_msg = self.get_max_timeout(msgs) + if self.local_high_qc.view < max_msg.high_qc.view: + self.update_high_qc(max_msg.high_qc) + if self.overlay.member_of_root_committee(self.id) and self.overlay.member_of_leaf_committee(self.id): + timeout_qc = build_timeout_qc(msgs) + self.update_timeout_qc(timeout_qc) + else: + self.update_timeout_qc(msgs.pop().timeout_qc) - def send(self, vote: Vote, *ids: Id): + def timeout_qc(self,timeout_qc: TimeoutQc): + pass + + def send(self, vote: Vote | Timeout, *ids: Id): pass def broadcast(self, block): @@ -272,15 +349,19 @@ class Carnot: return can_commit = ( parent.view == (grand_parent.view + 1) and - isinstance(block.qc, (StandardQc, )) and - isinstance(parent.qc, (StandardQc, )) + isinstance(block.qc, (StandardQc,)) and + isinstance(parent.qc, (StandardQc,)) ) if can_commit: self.committed_blocks[grand_parent.id()] = grand_parent + self.increment_latest_committed_view(grand_parent.view) def increment_voted_view(self, view: View): self.highest_voted_view = max(view, self.highest_voted_view) + def increment_latest_committed_view(self, view: View): + self.latest_committed_view = max(view, self.latest_committed_view) + def increment_view_qc(self, qc: Qc) -> bool: if qc.view < self.current_view: return False @@ -288,6 +369,19 @@ class Carnot: self.current_view = qc.view + 1 return True + def increment_view_timeout_qc(self, timeout_qc: TimeoutQc): + if timeout_qc is None or timeout_qc.view < self.current_view: + return + self.last_timeout_view_qc = timeout_qc + self.current_view = self.last_timeout_view_qc.view + 1 + return True + + @staticmethod + def get_max_timeout(timeouts: Set[Timeout]) -> Optional[Timeout]: + if not timeouts: + return None + return max(timeouts, key=lambda time: time.qc.view) + if __name__ == "__main__": pass diff --git a/carnot/test_happy_path.py b/carnot/test_happy_path.py index eba3d50..b8e45d5 100644 --- a/carnot/test_happy_path.py +++ b/carnot/test_happy_path.py @@ -35,6 +35,10 @@ class TestCarnotHappyPath(TestCase): carnot.receive_block(block4) self.assertEqual(len(carnot.safe_blocks), 5) # next block is duplicated and as it is already processed should be skipped + + # In my opinion duplicated block is rejected because both blocks have the same ID. + # In reality the IDs of blocks for the same view can be different if we compute ID based on + # the hash of the block content. What do you think? block5 = Block(view=4, qc=StandardQc(block=block3.id(), view=3)) carnot.receive_block(block5) self.assertEqual(len(carnot.safe_blocks), 5) @@ -145,10 +149,36 @@ class TestCarnotHappyPath(TestCase): carnot.receive_block(block5) self.assertEqual(len(carnot.safe_blocks), 5) - # Test cases for vote: - # 1: If a node votes for same block twice - def test_vote_for_received_block(self): + def test_receive_block_and_verify_if_latest_committed_block_and_high_qc_is_updated(self): + carnot = Carnot(int_to_id(0)) + genesis_block = self.add_genesis_block(carnot) + # 1 + block1 = Block(view=1, qc=StandardQc(block=genesis_block.id(), view=0)) + carnot.receive_block(block1) + # 2 + block2 = Block(view=2, qc=StandardQc(block=block1.id(), view=1)) + carnot.receive_block(block2) + + # 3 + block3 = Block(view=3, qc=StandardQc(block=block2.id(), view=2)) + carnot.receive_block(block3) + # 4 + block4 = Block(view=4, qc=StandardQc(block=block3.id(), view=3)) + carnot.receive_block(block4) + + self.assertEqual(len(carnot.safe_blocks), 5) + block5 = Block(view=5, qc=StandardQc(block=block4.id(), view=4)) + carnot.receive_block(block5) + self.assertEqual(carnot.latest_committed_view, 3) + self.assertEqual(carnot.local_high_qc.view, 4) + + # Test cases for vote: + def test_vote_for_received_block(self): + """ + 1: Votes received should increment highest_voted_view and current_view but should not change + latest_committed_view and last_timeout_view + """ class MockOverlay(Overlay): def member_of_root_com(self, _id: Id) -> bool: return False @@ -179,19 +209,43 @@ class TestCarnotHappyPath(TestCase): carnot.vote(block1, votes) self.assertEqual(carnot.highest_voted_view, 1) self.assertEqual(carnot.current_view, 1) + self.assertEqual(carnot.latest_committed_view, 0) + self.assertEqual(carnot.last_timeout_view, None) - # 2: If a node votes for two different blocks in the same view. - # 3: If a node in parent committee votes before it receives threshold of children's votes - # 4: If a node counts duplicate votes - # 6: If a node counts votes of nodes other than it's child committees. - # 7: If a node counts distinct votes for a safe block from its child committees. - # 8: If 7 is true, will the node vote for the mentioned safe block - - - - + def test_vote_for_received_block_if_threshold_votes_has_not_reached(self): + """ + 2 If last_voted_view is incremented after calling vote with votes lower than. + """ + class MockOverlay(Overlay): + def member_of_root_com(self, _id: Id) -> bool: + return False + def child_committee(self, parent: Id, child: Id) -> bool: + return True + def super_majority_threshold(self, _id: Id) -> int: + return 10 + def parent_committee(self, _id: Id) -> Optional[Committee]: + return set() + carnot = Carnot(int_to_id(0)) + carnot.overlay = MockOverlay() + genesis_block = self.add_genesis_block(carnot) + # 1 + block1 = Block(view=1, qc=StandardQc(block=genesis_block.id(), view=0)) + carnot.receive_block(block1) + votes = set( + Vote( + voter=int_to_id(i), + view=1, + block=block1.id(), + qc=StandardQc(block=block1.id(), view=1) + ) for i in range(10) + ) + carnot.vote(block1, votes) + # The test passes as asserting fails in len(votes) == self.overlay.super_majority_threshold(self.id) + # when number of votes are < 9 + self.assertEqual(carnot.highest_voted_view, 1) + self.assertEqual(carnot.current_view, 1)