mirror of
https://github.com/logos-co/nomos-specs.git
synced 2025-02-12 23:36:29 +00:00
Start unhappy path and update tests (#10)
* Tests for updating latest_committed_view and high_qc. * Vote tests * Vote tests * Get max timeout by highQC * Received Votes tests * Receive timeout msgs * Receive timeout msgs * Remove local files * Stylish, adjustments and fixes --------- Co-authored-by: mjalalzai <33738574+MForensic@users.noreply.github.com>
This commit is contained in:
parent
d056c6b2ab
commit
236fbab30a
118
carnot/carnot.py
118
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
|
||||
|
@ -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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user