diff --git a/carnot/carnot.py b/carnot/carnot.py index 6999441..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) @@ -201,10 +202,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 +315,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 +323,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): 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)