From f596893cd3bc915ac64ee700b526baea759dd54c Mon Sep 17 00:00:00 2001 From: mjalalzai <33738574+MForensic@users.noreply.github.com> Date: Fri, 31 Mar 2023 14:19:11 -0700 Subject: [PATCH 01/12] Tests for updating latest_committed_view and high_qc. --- carnot/carnot.py | 10 +++++-- carnot/test_happy_path.py | 61 ++++++++++++++++++++++++++++++++------- requirements.txt | 0 setup.py | 12 ++++++++ 4 files changed, 71 insertions(+), 12 deletions(-) create mode 100644 requirements.txt create mode 100644 setup.py diff --git a/carnot/carnot.py b/carnot/carnot.py index 6184f93..874c016 100644 --- a/carnot/carnot.py +++ b/carnot/carnot.py @@ -34,6 +34,7 @@ class AggregateQc: Qc: TypeAlias = StandardQc | AggregateQc + @dataclass class Block: view: View @@ -74,6 +75,7 @@ class Overlay: """ Overlay structure for a View """ + @abstractmethod def is_leader(self, _id: Id): """ @@ -272,15 +274,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 diff --git a/carnot/test_happy_path.py b/carnot/test_happy_path.py index eba3d50..0278dee 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,56 @@ class TestCarnotHappyPath(TestCase): carnot.receive_block(block5) self.assertEqual(len(carnot.safe_blocks), 5) + def test_receive_block_and_verify_if_latest_committed_view_is_incremented(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) + + def test_receive_block_and_verify_if_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.local_high_qc.view, 4) + # Test cases for vote: # 1: If a node votes for same block twice - def test_vote_for_received_block(self): + def test_vote_for_received_block(self): class MockOverlay(Overlay): def member_of_root_com(self, _id: Id) -> bool: return False @@ -186,12 +236,3 @@ class TestCarnotHappyPath(TestCase): # 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 - - - - - - - - - diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..e69de29 diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..756c06e --- /dev/null +++ b/setup.py @@ -0,0 +1,12 @@ +from setuptools import setup + +setup( + name='nomos-specs', + version='', + packages=['carnot'], + url='', + license='', + author='mohammad-az', + author_email='', + description='' +) From 7d0e84fd325d333411cc45d36ead23d7e6a4dbf4 Mon Sep 17 00:00:00 2001 From: mjalalzai <33738574+MForensic@users.noreply.github.com> Date: Sat, 1 Apr 2023 00:35:13 -0700 Subject: [PATCH 02/12] Vote tests --- carnot/test_happy_path.py | 71 ++++++++++++++++++++++----------------- 1 file changed, 41 insertions(+), 30 deletions(-) diff --git a/carnot/test_happy_path.py b/carnot/test_happy_path.py index 0278dee..e05ddfe 100644 --- a/carnot/test_happy_path.py +++ b/carnot/test_happy_path.py @@ -149,7 +149,7 @@ class TestCarnotHappyPath(TestCase): carnot.receive_block(block5) self.assertEqual(len(carnot.safe_blocks), 5) - def test_receive_block_and_verify_if_latest_committed_view_is_incremented(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 @@ -171,32 +171,10 @@ class TestCarnotHappyPath(TestCase): block5 = Block(view=5, qc=StandardQc(block=block4.id(), view=4)) carnot.receive_block(block5) self.assertEqual(carnot.latest_committed_view, 3) - - def test_receive_block_and_verify_if_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.local_high_qc.view, 4) # Test cases for vote: - # 1: If a node votes for same block twice + # 1: If a node votes def test_vote_for_received_block(self): class MockOverlay(Overlay): @@ -230,9 +208,42 @@ class TestCarnotHappyPath(TestCase): self.assertEqual(carnot.highest_voted_view, 1) self.assertEqual(carnot.current_view, 1) - # 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 + #2 If last_voted_view is incremented after calling vote. + + def test_vote_for_received_block_if_threshold_votes_has_not_reached(self): + 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(9) + ) + carnot.vote(block1, votes) + self.assertEqual(carnot.highest_voted_view, 0) + self.assertEqual(carnot.current_view, 0) + + + + + + From 30ebd96ebb86f14b92f5899c45e902911aa32449 Mon Sep 17 00:00:00 2001 From: mjalalzai <33738574+MForensic@users.noreply.github.com> Date: Sat, 1 Apr 2023 08:09:39 -0700 Subject: [PATCH 03/12] Vote tests --- carnot/test_happy_path.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/carnot/test_happy_path.py b/carnot/test_happy_path.py index e05ddfe..5251b90 100644 --- a/carnot/test_happy_path.py +++ b/carnot/test_happy_path.py @@ -208,7 +208,7 @@ class TestCarnotHappyPath(TestCase): self.assertEqual(carnot.highest_voted_view, 1) self.assertEqual(carnot.current_view, 1) - #2 If last_voted_view is incremented after calling vote. + #2 If last_voted_view is incremented after calling vote with votes lower than. def test_vote_for_received_block_if_threshold_votes_has_not_reached(self): class MockOverlay(Overlay): From f55d8971041dc654e1a1ace363abbb759c8bc214 Mon Sep 17 00:00:00 2001 From: mjalalzai <33738574+MForensic@users.noreply.github.com> Date: Sat, 1 Apr 2023 08:09:39 -0700 Subject: [PATCH 04/12] Get max timeout by highQC --- carnot/carnot.py | 13 ++++++++++++- carnot/test_happy_path.py | 8 ++++---- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/carnot/carnot.py b/carnot/carnot.py index 874c016..d8cf332 100644 --- a/carnot/carnot.py +++ b/carnot/carnot.py @@ -68,7 +68,13 @@ class TimeoutQc: high_qc: AggregateQc -Quorum: TypeAlias = Set[Vote] | Set[TimeoutQc] +@dataclass +class Timeout: + view: View + high_qc: Qc + + +Quorum: TypeAlias = Set[Vote] | Set[Timeout] class Overlay: @@ -294,6 +300,11 @@ class Carnot: self.current_view = qc.view + 1 return True + def get_max_timeout(timeouts: List[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 e05ddfe..35cab65 100644 --- a/carnot/test_happy_path.py +++ b/carnot/test_happy_path.py @@ -208,7 +208,7 @@ class TestCarnotHappyPath(TestCase): self.assertEqual(carnot.highest_voted_view, 1) self.assertEqual(carnot.current_view, 1) - #2 If last_voted_view is incremented after calling vote. + #2 If last_voted_view is incremented after calling vote with votes lower than. def test_vote_for_received_block_if_threshold_votes_has_not_reached(self): class MockOverlay(Overlay): @@ -236,11 +236,11 @@ class TestCarnotHappyPath(TestCase): view=1, block=block1.id(), qc=StandardQc(block=block1.id(), view=1) - ) for i in range(9) + ) for i in range(10) ) carnot.vote(block1, votes) - self.assertEqual(carnot.highest_voted_view, 0) - self.assertEqual(carnot.current_view, 0) + self.assertEqual(carnot.highest_voted_view, 1) + self.assertEqual(carnot.current_view, 1) From 523ec81577795656922ad8f1d0d82f6e45352aa3 Mon Sep 17 00:00:00 2001 From: mjalalzai <33738574+MForensic@users.noreply.github.com> Date: Sat, 1 Apr 2023 15:25:45 -0700 Subject: [PATCH 05/12] Received Votes tests --- carnot/test_happy_path.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/carnot/test_happy_path.py b/carnot/test_happy_path.py index 35cab65..94275ad 100644 --- a/carnot/test_happy_path.py +++ b/carnot/test_happy_path.py @@ -174,7 +174,8 @@ class TestCarnotHappyPath(TestCase): self.assertEqual(carnot.local_high_qc.view, 4) # Test cases for vote: - # 1: If a node votes + # 1: Votes received should increment highest_voted_view and current_view but should not change + # latest_committed_view and last_timeout_view def test_vote_for_received_block(self): class MockOverlay(Overlay): @@ -207,8 +208,10 @@ 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 last_voted_view is incremented after calling vote with votes lower than. + # 2 If last_voted_view is incremented after calling vote with votes lower than. def test_vote_for_received_block_if_threshold_votes_has_not_reached(self): class MockOverlay(Overlay): @@ -239,11 +242,8 @@ class TestCarnotHappyPath(TestCase): ) for i in range(10) ) carnot.vote(block1, votes) - self.assertEqual(carnot.highest_voted_view, 1) - self.assertEqual(carnot.current_view, 1) - - - - - + #### The test passes as the assert 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) From 4d73ae89c35132e24e2b88654e5120d95adb7205 Mon Sep 17 00:00:00 2001 From: mjalalzai <33738574+MForensic@users.noreply.github.com> Date: Sun, 2 Apr 2023 16:58:37 -0700 Subject: [PATCH 06/12] Receive timeout msgs --- carnot/carnot.py | 76 +++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 69 insertions(+), 7 deletions(-) diff --git a/carnot/carnot.py b/carnot/carnot.py index d8cf332..5fdeb47 100644 --- a/carnot/carnot.py +++ b/carnot/carnot.py @@ -65,13 +65,22 @@ class Vote: @dataclass class TimeoutQc: view: View - high_qc: AggregateQc + high_qc: Qc + qc_views: list[View] + SenderIds: Set[Id] + Sender: Id +# 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] @@ -90,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: @@ -166,6 +189,10 @@ def download(view) -> Block: raise NotImplementedError +def build_timeoutqc(msgs) -> TimeoutQc: + pass + + class Carnot: def __init__(self, _id: Id): self.id: Id = _id @@ -190,6 +217,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): @@ -201,6 +229,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: @@ -258,12 +293,32 @@ class Carnot: 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(): + 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() + ) - def timeout(self, view: View, msgs: Set["TimeoutMsg"]): - raise NotImplementedError() + self.broadcast(timeout_Msg) + + def timeout(self, view: View, 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_timeoutqc(msgs) + self.update_timeout_qc(timeout_qc) + else: + self.update_timeout_qc(msgs.pop().timeout_qc) + raise NotImplementedError() def send(self, vote: Vote, *ids: Id): pass @@ -300,7 +355,14 @@ class Carnot: self.current_view = qc.view + 1 return True - def get_max_timeout(timeouts: List[Timeout]) -> Optional[Timeout]: + def increment_view_timeout_qc(self, timeoutqc: TimeoutQc): + if timeoutqc == None or timeoutqc.view < self.current_view: + return + self.last_timeout_view_qc = timeoutqc + self.current_view = self.last_timeout_view_qc.view + 1 + return True + + def get_max_timeout(self, timeouts: Set[Timeout]) -> Optional[Timeout]: if not timeouts: return None return max(timeouts, key=lambda time: time.qc.view) From 9aba275f7270ddb12f3eb94dfa12396406beadea Mon Sep 17 00:00:00 2001 From: mjalalzai <33738574+MForensic@users.noreply.github.com> Date: Mon, 3 Apr 2023 00:02:04 -0700 Subject: [PATCH 07/12] Receive timeout msgs --- carnot/carnot.py | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/carnot/carnot.py b/carnot/carnot.py index 5fdeb47..f29a0e4 100644 --- a/carnot/carnot.py +++ b/carnot/carnot.py @@ -170,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: """ @@ -291,7 +303,6 @@ 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) if self.overlay.member_of_leaf_committee(self.id) or self.overlay.is_child_of_root(): timeout_Msg: Timeout = Timeout( @@ -303,10 +314,11 @@ class Carnot: timeout_qc=self.last_timeout_view_qc, sender=self.id() ) + self.send(timeout_Msg, *self.overlay.root_committee(self.id)) + for child_committee in self.overlay.child_of_root_committee(): + self.send(timeout_Msg, child_committee) - self.broadcast(timeout_Msg) - - def timeout(self, view: View, msgs: Set["Timeout"]): + 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 @@ -318,7 +330,9 @@ class Carnot: self.update_timeout_qc(timeout_qc) else: self.update_timeout_qc(msgs.pop().timeout_qc) - raise NotImplementedError() + + def timeout_qc(self,timeout_qc: TimeoutQc): + pass def send(self, vote: Vote, *ids: Id): pass From 85066df8322b284eba4a977e859b50f3e0b4df49 Mon Sep 17 00:00:00 2001 From: danielsanchezq Date: Mon, 3 Apr 2023 10:37:13 +0200 Subject: [PATCH 08/12] Remove local files --- requirements.txt | 0 setup.py | 12 ------------ 2 files changed, 12 deletions(-) delete mode 100644 requirements.txt delete mode 100644 setup.py diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index e69de29..0000000 diff --git a/setup.py b/setup.py deleted file mode 100644 index 756c06e..0000000 --- a/setup.py +++ /dev/null @@ -1,12 +0,0 @@ -from setuptools import setup - -setup( - name='nomos-specs', - version='', - packages=['carnot'], - url='', - license='', - author='mohammad-az', - author_email='', - description='' -) From 00b650255c068e9efd1eef9d87cc15321d096554 Mon Sep 17 00:00:00 2001 From: danielsanchezq Date: Mon, 3 Apr 2023 11:02:58 +0200 Subject: [PATCH 09/12] Stylish, adjustments and fixes --- carnot/carnot.py | 33 +++++++++++++++++---------------- carnot/test_happy_path.py | 22 ++++++++++++---------- 2 files changed, 29 insertions(+), 26 deletions(-) diff --git a/carnot/carnot.py b/carnot/carnot.py index f29a0e4..6999441 100644 --- a/carnot/carnot.py +++ b/carnot/carnot.py @@ -66,9 +66,9 @@ class Vote: class TimeoutQc: view: View high_qc: Qc - qc_views: list[View] - SenderIds: Set[Id] - Sender: Id + qc_views: List[View] + sender_ids: Set[Id] + sender: Id # local timeout field is only used by the root committee and its children when they timeout. The timeout_qc is built @@ -148,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 @@ -201,7 +201,7 @@ def download(view) -> Block: raise NotImplementedError -def build_timeoutqc(msgs) -> TimeoutQc: +def build_timeout_qc(msgs) -> TimeoutQc: pass @@ -304,19 +304,19 @@ class Carnot: def local_timeout(self, new_overlay: Overlay): self.increment_voted_view(self.current_view) - if self.overlay.member_of_leaf_committee(self.id) or self.overlay.is_child_of_root(): - timeout_Msg: Timeout = Timeout( + 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() + sender=self.id ) - self.send(timeout_Msg, *self.overlay.root_committee(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) + self.send(timeout_msg, child_committee) def timeout(self, msgs: Set["Timeout"]): assert len(msgs) == self.overlay.super_majority_threshold(self.id) @@ -326,7 +326,7 @@ class Carnot: 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_timeoutqc(msgs) + timeout_qc = build_timeout_qc(msgs) self.update_timeout_qc(timeout_qc) else: self.update_timeout_qc(msgs.pop().timeout_qc) @@ -334,7 +334,7 @@ class Carnot: def timeout_qc(self,timeout_qc: TimeoutQc): pass - def send(self, vote: Vote, *ids: Id): + def send(self, vote: Vote | Timeout, *ids: Id): pass def broadcast(self, block): @@ -369,14 +369,15 @@ class Carnot: self.current_view = qc.view + 1 return True - def increment_view_timeout_qc(self, timeoutqc: TimeoutQc): - if timeoutqc == None or timeoutqc.view < self.current_view: + 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 = timeoutqc + self.last_timeout_view_qc = timeout_qc self.current_view = self.last_timeout_view_qc.view + 1 return True - def get_max_timeout(self, timeouts: Set[Timeout]) -> Optional[Timeout]: + @staticmethod + def get_max_timeout(timeouts: Set[Timeout]) -> Optional[Timeout]: if not timeouts: return None return max(timeouts, key=lambda time: time.qc.view) diff --git a/carnot/test_happy_path.py b/carnot/test_happy_path.py index 94275ad..b8e45d5 100644 --- a/carnot/test_happy_path.py +++ b/carnot/test_happy_path.py @@ -173,11 +173,12 @@ class TestCarnotHappyPath(TestCase): self.assertEqual(carnot.latest_committed_view, 3) self.assertEqual(carnot.local_high_qc.view, 4) - # Test cases for vote: - # 1: Votes received should increment highest_voted_view and current_view but should not change - # latest_committed_view and last_timeout_view - + # 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 @@ -211,9 +212,10 @@ class TestCarnotHappyPath(TestCase): self.assertEqual(carnot.latest_committed_view, 0) self.assertEqual(carnot.last_timeout_view, None) - # 2 If last_voted_view is incremented after calling vote with votes lower than. - 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 @@ -243,7 +245,7 @@ class TestCarnotHappyPath(TestCase): ) carnot.vote(block1, votes) - #### The test passes as the assert 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) + # 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) From 25560c076cd8009aac34608ca34c8de0b581b4f0 Mon Sep 17 00:00:00 2001 From: danielsanchezq Date: Mon, 3 Apr 2023 11:09:11 +0200 Subject: [PATCH 10/12] Update build timeout qc test --- carnot/carnot.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/carnot/carnot.py b/carnot/carnot.py index 6999441..8ab9577 100644 --- a/carnot/carnot.py +++ b/carnot/carnot.py @@ -201,10 +201,6 @@ def download(view) -> Block: raise NotImplementedError -def build_timeout_qc(msgs) -> TimeoutQc: - pass - - class Carnot: def __init__(self, _id: Id): self.id: Id = _id @@ -318,7 +314,7 @@ class Carnot: for child_committee in self.overlay.child_of_root_committee(): self.send(timeout_msg, child_committee) - def timeout(self, msgs: Set["Timeout"]): + 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 @@ -326,12 +322,12 @@ class Carnot: 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) + timeout_qc = self.build_timeout_qc(msgs) self.update_timeout_qc(timeout_qc) else: self.update_timeout_qc(msgs.pop().timeout_qc) - def timeout_qc(self,timeout_qc: TimeoutQc): + def build_timeout_qc(self, msgs: Set[Timeout]) -> TimeoutQc: pass def send(self, vote: Vote | Timeout, *ids: Id): From ac09aaf9eba44b1194005d1b0295a692dd5740e2 Mon Sep 17 00:00:00 2001 From: danielsanchezq Date: Mon, 3 Apr 2023 13:49:43 +0200 Subject: [PATCH 11/12] Added block content --- carnot/carnot.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/carnot/carnot.py b/carnot/carnot.py index 8ab9577..0c45957 100644 --- a/carnot/carnot.py +++ b/carnot/carnot.py @@ -1,5 +1,5 @@ from dataclasses import dataclass -from typing import TypeAlias, List, Set, Self, Optional, Dict +from typing import TypeAlias, List, Set, Self, Optional, Dict, FrozenSet from abc import abstractmethod Id: TypeAlias = bytes @@ -39,6 +39,7 @@ Qc: TypeAlias = StandardQc | AggregateQc class Block: view: View qc: Qc + content: FrozenSet[Id] def extends(self, ancestor: Self) -> bool: """ @@ -51,7 +52,7 @@ class Block: return self.qc.block def id(self) -> Id: - return int_to_id(hash((self.view, self.qc.view, self.qc.block))) + return int_to_id(hash(self.content)) @dataclass(unsafe_hash=True) From f92bb0bf15df52618cc56b13d64674ff5d3a38a1 Mon Sep 17 00:00:00 2001 From: danielsanchezq Date: Mon, 3 Apr 2023 13:49:54 +0200 Subject: [PATCH 12/12] Fix tests with block content and comments --- carnot/test_happy_path.py | 106 +++++++++++++++++++++----------------- 1 file changed, 58 insertions(+), 48 deletions(-) diff --git a/carnot/test_happy_path.py b/carnot/test_happy_path.py index b8e45d5..454769f 100644 --- a/carnot/test_happy_path.py +++ b/carnot/test_happy_path.py @@ -5,7 +5,7 @@ from unittest import TestCase class TestCarnotHappyPath(TestCase): @staticmethod def add_genesis_block(carnot: Carnot) -> Block: - genesis_block = Block(view=0, qc=StandardQc(block=b"", view=0)) + genesis_block = Block(view=0, qc=StandardQc(block=b"", view=0), content=frozenset(b"")) carnot.safe_blocks[genesis_block.id()] = genesis_block carnot.committed_blocks[genesis_block.id()] = genesis_block return genesis_block @@ -13,33 +13,33 @@ class TestCarnotHappyPath(TestCase): def test_receive_block(self): carnot = Carnot(int_to_id(0)) genesis_block = self.add_genesis_block(carnot) - block = Block(view=1, qc=StandardQc(block=genesis_block.id(), view=0)) + block = Block(view=1, qc=StandardQc(block=genesis_block.id(), view=0), content=frozenset(b"1")) carnot.receive_block(block) def test_receive_multiple_blocks_for_the_same_view(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)) + block1 = Block(view=1, qc=StandardQc(block=genesis_block.id(), view=0), content=frozenset(b"1")) carnot.receive_block(block1) # 2 - block2 = Block(view=2, qc=StandardQc(block=block1.id(), view=1)) + block2 = Block(view=2, qc=StandardQc(block=block1.id(), view=1), content=frozenset(b"2")) carnot.receive_block(block2) # 3 - block3 = Block(view=3, qc=StandardQc(block=block2.id(), view=2)) + block3 = Block(view=3, qc=StandardQc(block=block2.id(), view=2), content=frozenset(b"3")) carnot.receive_block(block3) # 4 - block4 = Block(view=4, qc=StandardQc(block=block3.id(), view=3)) + block4 = Block(view=4, qc=StandardQc(block=block3.id(), view=3), content=frozenset(b"4")) 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)) + block5 = Block(view=4, qc=StandardQc(block=block3.id(), view=3), content=frozenset(b"4")) + carnot.receive_block(block5) + self.assertEqual(len(carnot.safe_blocks), 5) + # next block has a different view but is duplicated and as it is already processed should be skipped + block5 = Block(view=5, qc=StandardQc(block=block3.id(), view=4), content=frozenset(b"4")) carnot.receive_block(block5) self.assertEqual(len(carnot.safe_blocks), 5) @@ -47,25 +47,26 @@ class TestCarnotHappyPath(TestCase): 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)) + block1 = Block(view=1, qc=StandardQc(block=genesis_block.id(), view=0), content=frozenset(b"1")) carnot.receive_block(block1) # 2 - block2 = Block(view=2, qc=StandardQc(block=block1.id(), view=1)) + block2 = Block(view=2, qc=StandardQc(block=block1.id(), view=1), content=frozenset(b"2")) carnot.receive_block(block2) # 3 - block3 = Block(view=3, qc=StandardQc(block=block2.id(), view=2)) + block3 = Block(view=3, qc=StandardQc(block=block2.id(), view=2), content=frozenset(b"3")) carnot.receive_block(block3) # 4 - block4 = Block(view=4, qc=StandardQc(block=block3.id(), view=3)) + block4 = Block(view=4, qc=StandardQc(block=block3.id(), view=3), content=frozenset(b"4")) carnot.receive_block(block4) self.assertEqual(len(carnot.safe_blocks), 5) - # This block should be rejected based on the condition below in block_is_safe(). + # This block should be rejected based on the condition below in block_is_safe(). # block.view >= self.latest_committed_view and block.view == (standard.view + 1) # block_is_safe() should return false. - block5 = Block(view=3, qc=StandardQc(block=block4.id(), view=4)) + block5 = Block(view=3, qc=StandardQc(block=block4.id(), view=4), content=frozenset(b"5")) + self.assertFalse(carnot.block_is_safe(block5)) carnot.receive_block(block5) self.assertEqual(len(carnot.safe_blocks), 5) @@ -73,79 +74,85 @@ class TestCarnotHappyPath(TestCase): 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)) + block1 = Block(view=1, qc=StandardQc(block=genesis_block.id(), view=0), content=frozenset(b"1")) carnot.receive_block(block1) # 2 - block2 = Block(view=2, qc=StandardQc(block=block1.id(), view=1)) + block2 = Block(view=2, qc=StandardQc(block=block1.id(), view=1), content=frozenset(b"2")) carnot.receive_block(block2) # 3 - block3 = Block(view=3, qc=StandardQc(block=block2.id(), view=2)) + block3 = Block(view=3, qc=StandardQc(block=block2.id(), view=2), content=frozenset(b"3")) carnot.receive_block(block3) # 4 - block4 = Block(view=4, qc=StandardQc(block=block3.id(), view=3)) + block4 = Block(view=4, qc=StandardQc(block=block3.id(), view=3), content=frozenset(b"4")) carnot.receive_block(block4) self.assertEqual(len(carnot.safe_blocks), 5) - # 5 This is the old standard qc of block number 3. For standarnd QC we must always have qc.view==block.view-1. + # 5 This is the old standard qc of block number 3. For standard QC we must always have qc.view==block.view-1. # This block should be rejected based on the condition below in block_is_safe(). - # block.view >= self.latest_committed_view and block.view == (standard.view + 1) + # block.view >= self.latest_committed_view and block.view == (standard.view + 1) # block_is_safe() should return false. - block5 = Block(view=5, qc=StandardQc(block=block3.id(), view=3)) + block5 = Block(view=5, qc=StandardQc(block=block3.id(), view=3), content=frozenset(b"5")) + self.assertFalse(carnot.block_is_safe(block5)) carnot.receive_block(block5) self.assertEqual(len(carnot.safe_blocks), 5) - # Any block with block.view < 4 must be committed def test_receive_block_and_commit_its_grand_parent_chain(self): + """ + Any block with block.view < 4 must be committed + """ 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)) + block1 = Block(view=1, qc=StandardQc(block=genesis_block.id(), view=0), content=frozenset(b"1")) carnot.receive_block(block1) # 2 - block2 = Block(view=2, qc=StandardQc(block=block1.id(), view=1)) + block2 = Block(view=2, qc=StandardQc(block=block1.id(), view=1), content=frozenset(b"2")) carnot.receive_block(block2) # 3 - block3 = Block(view=3, qc=StandardQc(block=block2.id(), view=2)) + block3 = Block(view=3, qc=StandardQc(block=block2.id(), view=2), content=frozenset(b"3")) carnot.receive_block(block3) # 4 - block4 = Block(view=4, qc=StandardQc(block=block3.id(), view=3)) + block4 = Block(view=4, qc=StandardQc(block=block3.id(), view=3), content=frozenset(b"4")) carnot.receive_block(block4) - block5 = Block(view=5, qc=StandardQc(block=block4.id(), view=4)) + block5 = Block(view=5, qc=StandardQc(block=block4.id(), view=4), content=frozenset(b"5")) carnot.receive_block(block5) for block in (block1, block2, block3): self.assertIn(block.id(), carnot.committed_blocks) - # Block3 must be committed as it is the grandparent of block5. Hence, it should not be possible - # to avert it. def test_receive_block_has_an_old_qc_and_tries_to_revert_a_committed_block(self): + """ + Block3 must be committed as it is the grandparent of block5. Hence, it should not be possible + to avert it. + """ 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)) + block1 = Block(view=1, qc=StandardQc(block=genesis_block.id(), view=0), content=frozenset(b"1")) carnot.receive_block(block1) # 2 - block2 = Block(view=2, qc=StandardQc(block=block1.id(), view=1)) + block2 = Block(view=2, qc=StandardQc(block=block1.id(), view=1), content=frozenset(b"2")) carnot.receive_block(block2) # 3 - block3 = Block(view=3, qc=StandardQc(block=block2.id(), view=2)) + block3 = Block(view=3, qc=StandardQc(block=block2.id(), view=2), content=frozenset(b"3")) carnot.receive_block(block3) # 4 - block4 = Block(view=4, qc=StandardQc(block=block3.id(), view=3)) + block4 = Block(view=4, qc=StandardQc(block=block3.id(), view=3), content=frozenset(b"4")) carnot.receive_block(block4) self.assertEqual(len(carnot.safe_blocks), 5) # 5 This is the old standard qc of block number 2. By using the QC for block2, block5 tries to form a fork # to avert block3 and block b4. Block3 is a committed block # block_is_safe() should return false. - block5 = Block(view=5, qc=StandardQc(block=block2.id(), view=2)) + block5 = Block(view=5, qc=StandardQc(block=block2.id(), view=2), content=frozenset(b"5")) + self.assertFalse(carnot.block_is_safe(block5)) carnot.receive_block(block5) self.assertEqual(len(carnot.safe_blocks), 5) @@ -153,22 +160,22 @@ class TestCarnotHappyPath(TestCase): 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)) + block1 = Block(view=1, qc=StandardQc(block=genesis_block.id(), view=0), content=frozenset(b"1")) carnot.receive_block(block1) # 2 - block2 = Block(view=2, qc=StandardQc(block=block1.id(), view=1)) + block2 = Block(view=2, qc=StandardQc(block=block1.id(), view=1), content=frozenset(b"2")) carnot.receive_block(block2) # 3 - block3 = Block(view=3, qc=StandardQc(block=block2.id(), view=2)) + block3 = Block(view=3, qc=StandardQc(block=block2.id(), view=2), content=frozenset(b"3")) carnot.receive_block(block3) # 4 - block4 = Block(view=4, qc=StandardQc(block=block3.id(), view=3)) + block4 = Block(view=4, qc=StandardQc(block=block3.id(), view=3), content=frozenset(b"4")) carnot.receive_block(block4) self.assertEqual(len(carnot.safe_blocks), 5) - block5 = Block(view=5, qc=StandardQc(block=block4.id(), view=4)) + block5 = Block(view=5, qc=StandardQc(block=block4.id(), view=4), content=frozenset(b"5")) carnot.receive_block(block5) self.assertEqual(carnot.latest_committed_view, 3) self.assertEqual(carnot.local_high_qc.view, 4) @@ -196,7 +203,7 @@ class TestCarnotHappyPath(TestCase): carnot.overlay = MockOverlay() genesis_block = self.add_genesis_block(carnot) # 1 - block1 = Block(view=1, qc=StandardQc(block=genesis_block.id(), view=0)) + block1 = Block(view=1, qc=StandardQc(block=genesis_block.id(), view=0), content=frozenset(b"1")) carnot.receive_block(block1) votes = set( Vote( @@ -233,19 +240,22 @@ class TestCarnotHappyPath(TestCase): carnot.overlay = MockOverlay() genesis_block = self.add_genesis_block(carnot) # 1 - block1 = Block(view=1, qc=StandardQc(block=genesis_block.id(), view=0)) + block1 = Block(view=1, qc=StandardQc(block=genesis_block.id(), view=0), content=frozenset(b"1")) 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) + ) for i in range(3) ) - carnot.vote(block1, votes) + + with self.assertRaises((AssertionError, )): + 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) + self.assertEqual(carnot.highest_voted_view, 0) + self.assertEqual(carnot.current_view, 0)