mirror of
https://github.com/logos-co/nomos-specs.git
synced 2025-02-19 10:47:20 +00:00
update view upon reception of timeout qc
This commit is contained in:
parent
690cc071db
commit
58ebc6aa0f
@ -238,7 +238,7 @@ class Carnot:
|
|||||||
# and the current view is updated to qc.view+1
|
# and the current view is updated to qc.view+1
|
||||||
self.current_view: View = 0
|
self.current_view: View = 0
|
||||||
# Highest voted view counter. This is used to prevent a node from voting twice or vote after timeout.
|
# Highest voted view counter. This is used to prevent a node from voting twice or vote after timeout.
|
||||||
self.highest_voted_view: View = 0
|
self.highest_voted_view: View = -1
|
||||||
# This is most recent (in terms of view) Standard QC that has been received by the node
|
# This is most recent (in terms of view) Standard QC that has been received by the node
|
||||||
self.local_high_qc: Optional[Qc] = None
|
self.local_high_qc: Optional[Qc] = None
|
||||||
# Validated blocks with their validated QCs are included here. If commit conditions are satisfied for
|
# Validated blocks with their validated QCs are included here. If commit conditions are satisfied for
|
||||||
@ -299,18 +299,9 @@ class Carnot:
|
|||||||
return committed_blocks
|
return committed_blocks
|
||||||
|
|
||||||
def block_is_safe(self, block: Block) -> bool:
|
def block_is_safe(self, block: Block) -> bool:
|
||||||
match block.qc:
|
|
||||||
case StandardQc() as standard:
|
|
||||||
return (
|
|
||||||
standard.view >= self.latest_committed_view() and
|
|
||||||
block.view == standard.view + 1
|
|
||||||
)
|
|
||||||
case AggregateQc() as aggregated:
|
|
||||||
if aggregated.high_qc().view < self.latest_committed_view():
|
|
||||||
return False
|
|
||||||
return (
|
return (
|
||||||
block.view >= self.current_view and
|
block.view >= self.current_view and
|
||||||
block.view == aggregated.view + 1
|
block.view == block.qc.view + 1
|
||||||
)
|
)
|
||||||
|
|
||||||
# Ask Dani
|
# Ask Dani
|
||||||
@ -357,7 +348,7 @@ class Carnot:
|
|||||||
assert len(votes) == self.overlay.super_majority_threshold(self.id)
|
assert len(votes) == self.overlay.super_majority_threshold(self.id)
|
||||||
assert all(self.overlay.is_member_of_child_committee(self.id, vote.voter) for vote in votes)
|
assert all(self.overlay.is_member_of_child_committee(self.id, vote.voter) for vote in votes)
|
||||||
assert all(vote.block == block.id() for vote in votes)
|
assert all(vote.block == block.id() for vote in votes)
|
||||||
assert block.view > self.highest_voted_view
|
assert self.highest_voted_view < block.view
|
||||||
|
|
||||||
if self.overlay.is_member_of_root_committee(self.id):
|
if self.overlay.is_member_of_root_committee(self.id):
|
||||||
qc = self.build_qc(block.view, block, None)
|
qc = self.build_qc(block.view, block, None)
|
||||||
@ -374,12 +365,14 @@ class Carnot:
|
|||||||
self.send(vote, self.overlay.leader(block.view + 1))
|
self.send(vote, self.overlay.leader(block.view + 1))
|
||||||
else:
|
else:
|
||||||
self.send(vote, *self.overlay.parent_committee(self.id))
|
self.send(vote, *self.overlay.parent_committee(self.id))
|
||||||
self.increment_voted_view(block.view) # to avoid voting again for this view.
|
|
||||||
self.reset_last_timeout_view_qc(block.qc)
|
self.highest_voted_view = block.view
|
||||||
|
|
||||||
def forward_vote(self, vote: Vote):
|
def forward_vote(self, vote: Vote):
|
||||||
assert vote.block in self.safe_blocks
|
assert vote.block in self.safe_blocks
|
||||||
assert self.overlay.is_member_of_child_committee(self.id, vote.voter)
|
assert self.overlay.is_member_of_child_committee(self.id, vote.voter)
|
||||||
|
# we only forward votes after we've voted ourselves
|
||||||
|
assert self.highest_voted_view >= vote.view
|
||||||
|
|
||||||
if self.overlay.is_member_of_root_committee(self.id):
|
if self.overlay.is_member_of_root_committee(self.id):
|
||||||
self.send(vote, self.overlay.leader(self.current_view + 1))
|
self.send(vote, self.overlay.leader(self.current_view + 1))
|
||||||
@ -387,6 +380,8 @@ class Carnot:
|
|||||||
def forward_new_view(self, msg: NewView):
|
def forward_new_view(self, msg: NewView):
|
||||||
assert msg.view == self.current_view
|
assert msg.view == self.current_view
|
||||||
assert self.overlay.is_member_of_child_committee(self.id, msg.sender)
|
assert self.overlay.is_member_of_child_committee(self.id, msg.sender)
|
||||||
|
# we only forward votes after we've voted ourselves
|
||||||
|
assert self.highest_voted_view >= vote.view
|
||||||
|
|
||||||
if self.overlay.is_member_of_root_committee(self.id):
|
if self.overlay.is_member_of_root_committee(self.id):
|
||||||
self.send(msg, self.overlay.leader(self.current_view + 1))
|
self.send(msg, self.overlay.leader(self.current_view + 1))
|
||||||
@ -468,7 +463,12 @@ class Carnot:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def local_timeout(self):
|
def local_timeout(self):
|
||||||
self.increment_voted_view(self.current_view)
|
"""
|
||||||
|
Root committee changes for each failure, so repeated failure will be handled by different
|
||||||
|
root committees
|
||||||
|
"""
|
||||||
|
# avoid voting after we timeout
|
||||||
|
self.highest_voted_view = self.current_view
|
||||||
|
|
||||||
if self.overlay.is_member_of_root_committee(self.id) or self.overlay.is_child_of_root_committee(self.id):
|
if self.overlay.is_member_of_root_committee(self.id) or self.overlay.is_child_of_root_committee(self.id):
|
||||||
timeout_msg: Timeout = Timeout(
|
timeout_msg: Timeout = Timeout(
|
||||||
@ -486,6 +486,7 @@ class Carnot:
|
|||||||
Root committee detected that supermajority of root + its children has timed out
|
Root committee detected that supermajority of root + its children has timed out
|
||||||
The view has failed and this information is sent to all participants along with the information
|
The view has failed and this information is sent to all participants along with the information
|
||||||
necessary to reconstruct the new overlay
|
necessary to reconstruct the new overlay
|
||||||
|
|
||||||
"""
|
"""
|
||||||
assert len(msgs) == self.overlay.leader_super_majority_threshold(self.id)
|
assert len(msgs) == self.overlay.leader_super_majority_threshold(self.id)
|
||||||
assert all(msg.view >= self.current_view for msg in msgs)
|
assert all(msg.view >= self.current_view for msg in msgs)
|
||||||
@ -493,10 +494,9 @@ class Carnot:
|
|||||||
assert self.overlay.is_member_of_root_committee(self.id)
|
assert self.overlay.is_member_of_root_committee(self.id)
|
||||||
|
|
||||||
timeout_qc = self.build_timeout_qc(msgs, self.id)
|
timeout_qc = self.build_timeout_qc(msgs, self.id)
|
||||||
self.update_timeout_qc(timeout_qc)
|
|
||||||
self.update_high_qc(timeout_qc.high_qc)
|
|
||||||
self.rebuild_overlay_from_timeout_qc(timeout_qc)
|
|
||||||
self.broadcast(timeout_qc) # we broadcast so all nodes can get ready for voting on a new view
|
self.broadcast(timeout_qc) # we broadcast so all nodes can get ready for voting on a new view
|
||||||
|
# TODO: this call could be avoided if `receive_timeout_qc` is triggered for all nodes
|
||||||
|
# self.receive_timeout_qc(timeout_qc)
|
||||||
|
|
||||||
# noinspection PyTypeChecker
|
# noinspection PyTypeChecker
|
||||||
def approve_new_view(self, timeout_qc: TimeoutQc, new_views: Set[NewView]):
|
def approve_new_view(self, timeout_qc: TimeoutQc, new_views: Set[NewView]):
|
||||||
@ -511,6 +511,10 @@ class Carnot:
|
|||||||
assert all(new_view.timeout_qc.view == timeout_qc.view for new_view in new_views)
|
assert all(new_view.timeout_qc.view == timeout_qc.view for new_view in new_views)
|
||||||
assert len(new_views) == self.overlay.super_majority_threshold(self.id)
|
assert len(new_views) == self.overlay.super_majority_threshold(self.id)
|
||||||
assert all(self.overlay.is_member_of_child_committee(self.id, new_view.sender) for new_view in new_views)
|
assert all(self.overlay.is_member_of_child_committee(self.id, new_view.sender) for new_view in new_views)
|
||||||
|
# the new view should be for the view successive to the timeout
|
||||||
|
assert all(timeout_qc.view + 1 == new_view.view for new_view in new_views)
|
||||||
|
view = timeout_qc.view + 1
|
||||||
|
assert self.highest_voted_view < view
|
||||||
|
|
||||||
# get the highest qc from the new views
|
# get the highest qc from the new views
|
||||||
messages_high_qc = (new_view.high_qc for new_view in new_views)
|
messages_high_qc = (new_view.high_qc for new_view in new_views)
|
||||||
@ -520,7 +524,8 @@ class Carnot:
|
|||||||
)
|
)
|
||||||
self.update_high_qc(high_qc)
|
self.update_high_qc(high_qc)
|
||||||
timeout_msg = NewView(
|
timeout_msg = NewView(
|
||||||
view=self.current_view,
|
view=view,
|
||||||
|
# TODO: even if this event is processed "later", we should not allow high_qc.view to be >= timeout_qc.view
|
||||||
high_qc=self.local_high_qc,
|
high_qc=self.local_high_qc,
|
||||||
sender=self.id,
|
sender=self.id,
|
||||||
timeout_qc=timeout_qc,
|
timeout_qc=timeout_qc,
|
||||||
@ -531,22 +536,18 @@ class Carnot:
|
|||||||
else:
|
else:
|
||||||
self.send(timeout_msg, *self.overlay.parent_committee(self.id))
|
self.send(timeout_msg, *self.overlay.parent_committee(self.id))
|
||||||
|
|
||||||
|
|
||||||
# This checks if a node has already incremented its voted view by local_timeout. If not then it should
|
# This checks if a node has already incremented its voted view by local_timeout. If not then it should
|
||||||
# do it now to avoid voting in this view.
|
# do it now to avoid voting in this view.
|
||||||
if self.highest_voted_view < self.current_view:
|
self.highest_voted_view = view
|
||||||
self.increment_voted_view(timeout_qc.view)
|
|
||||||
# Update our current view and go ahead with the next step
|
|
||||||
self.update_current_view_from_timeout_qc(timeout_qc)
|
|
||||||
|
|
||||||
# Just a suggestion that received_timeout_qc can be reused by each node when the process timeout_qc of the NewView msg.
|
# Just a suggestion that received_timeout_qc can be reused by each node when the process timeout_qc of the NewView msg.
|
||||||
def receive_timeout_qc(self, timeout_qc: TimeoutQc):
|
def receive_timeout_qc(self, timeout_qc: TimeoutQc):
|
||||||
# assert timeout_qc.view >= self.current_view
|
assert timeout_qc.view >= self.current_view
|
||||||
new_high_qc = timeout_qc.high_qc
|
new_high_qc = timeout_qc.high_qc
|
||||||
if new_high_qc.view > self.local_high_qc.view:
|
|
||||||
self.update_high_qc(new_high_qc)
|
self.update_high_qc(new_high_qc)
|
||||||
self.update_timeout_qc(timeout_qc)
|
self.update_timeout_qc(timeout_qc)
|
||||||
self.update_last_view_timeout_qc(timeout_qc)
|
# Update our current view and go ahead with the next step
|
||||||
|
self.update_current_view_from_timeout_qc(timeout_qc)
|
||||||
self.rebuild_overlay_from_timeout_qc(timeout_qc)
|
self.rebuild_overlay_from_timeout_qc(timeout_qc)
|
||||||
|
|
||||||
def rebuild_overlay_from_timeout_qc(self, timeout_qc: TimeoutQc):
|
def rebuild_overlay_from_timeout_qc(self, timeout_qc: TimeoutQc):
|
||||||
@ -570,19 +571,6 @@ class Carnot:
|
|||||||
def broadcast(self, block):
|
def broadcast(self, block):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def increment_voted_view(self, view: View):
|
|
||||||
self.highest_voted_view = max(view, self.highest_voted_view)
|
|
||||||
|
|
||||||
def update_last_view_timeout_qc(self, timeout_qc: TimeoutQc):
|
|
||||||
if timeout_qc is None or timeout_qc.view < self.current_view:
|
|
||||||
return
|
|
||||||
self.last_view_timeout_qc = timeout_qc
|
|
||||||
|
|
||||||
def reset_last_timeout_view_qc(self, qc: Qc):
|
|
||||||
if qc.view < self.current_view:
|
|
||||||
return
|
|
||||||
self.last_view_timeout_qc = None
|
|
||||||
|
|
||||||
def update_current_view_from_timeout_qc(self, timeout_qc: TimeoutQc):
|
def update_current_view_from_timeout_qc(self, timeout_qc: TimeoutQc):
|
||||||
self.current_view = timeout_qc.view + 1
|
self.current_view = timeout_qc.view + 1
|
||||||
|
|
||||||
|
@ -270,7 +270,7 @@ class TestCarnotHappyPath(TestCase):
|
|||||||
|
|
||||||
# The test passes as asserting fails in len(votes) == self.overlay.super_majority_threshold(self.id)
|
# The test passes as asserting fails in len(votes) == self.overlay.super_majority_threshold(self.id)
|
||||||
# when number of votes are < 9
|
# when number of votes are < 9
|
||||||
self.assertEqual(carnot.highest_voted_view, 0)
|
self.assertEqual(carnot.highest_voted_view, -1)
|
||||||
self.assertEqual(carnot.current_view, 1)
|
self.assertEqual(carnot.current_view, 1)
|
||||||
|
|
||||||
def test_initial_leader_proposes_and_advance(self):
|
def test_initial_leader_proposes_and_advance(self):
|
||||||
|
@ -92,7 +92,6 @@ def add_genesis_block(carnot: Carnot) -> Block:
|
|||||||
genesis_block = Block(view=0, qc=StandardQc(block=b"", view=0), _id=b"")
|
genesis_block = Block(view=0, qc=StandardQc(block=b"", view=0), _id=b"")
|
||||||
carnot.safe_blocks[genesis_block.id()] = genesis_block
|
carnot.safe_blocks[genesis_block.id()] = genesis_block
|
||||||
carnot.receive_block(genesis_block)
|
carnot.receive_block(genesis_block)
|
||||||
carnot.increment_voted_view(0)
|
|
||||||
carnot.local_high_qc = genesis_block.qc
|
carnot.local_high_qc = genesis_block.qc
|
||||||
carnot.current_view = 1
|
carnot.current_view = 1
|
||||||
return genesis_block
|
return genesis_block
|
||||||
@ -219,18 +218,25 @@ class TestCarnotUnhappyPath(TestCase):
|
|||||||
|
|
||||||
nodes, leader, proposed_block = setup_initial_setup(self, overlay, 5)
|
nodes, leader, proposed_block = setup_initial_setup(self, overlay, 5)
|
||||||
|
|
||||||
for view in range(1, 4):
|
# In this loop 'view' is the view that fails
|
||||||
|
for view in range(1, 4, 2):
|
||||||
|
# When view v fails, a timeout qc is built for view v and nodes jump to view v + 1
|
||||||
|
# while aggregating votes for the high qc. Those votes are then forwarded to the leader of view v + 2
|
||||||
|
# which can propose a block with those aggregate votes as proof of the previous round completion.
|
||||||
root_votes = fail(self, overlay, nodes, proposed_block)
|
root_votes = fail(self, overlay, nodes, proposed_block)
|
||||||
leader.propose_block(view+1, root_votes)
|
leader.propose_block(view+2, root_votes)
|
||||||
|
|
||||||
# Add final assertions on nodes
|
# Add final assertions on nodes
|
||||||
proposed_block = leader.latest_event
|
proposed_block = leader.latest_event
|
||||||
self.assertEqual(proposed_block.view, view + 1)
|
# Thus, the first block that can be proposed is 2 views after the timeout
|
||||||
self.assertEqual(proposed_block.qc.view, view)
|
self.assertEqual(proposed_block.view, view + 2)
|
||||||
|
# Its qc is always for the view before the block is proposed for
|
||||||
|
self.assertEqual(proposed_block.qc.view, view + 1)
|
||||||
|
# The high qc is 0, since we never had a successful round
|
||||||
self.assertEqual(proposed_block.qc.high_qc().view, 0)
|
self.assertEqual(proposed_block.qc.high_qc().view, 0)
|
||||||
self.assertEqual(leader.last_view_timeout_qc.view, view)
|
self.assertEqual(leader.last_view_timeout_qc.view, view)
|
||||||
self.assertEqual(leader.local_high_qc.view, 0)
|
self.assertEqual(leader.local_high_qc.view, 0)
|
||||||
self.assertEqual(leader.highest_voted_view, view)
|
self.assertEqual(leader.highest_voted_view, view+1)
|
||||||
|
|
||||||
for node in nodes.values():
|
for node in nodes.values():
|
||||||
self.assertEqual(node.latest_committed_view(), 0)
|
self.assertEqual(node.latest_committed_view(), 0)
|
||||||
@ -251,24 +257,24 @@ class TestCarnotUnhappyPath(TestCase):
|
|||||||
proposed_block = leader.latest_event
|
proposed_block = leader.latest_event
|
||||||
|
|
||||||
root_votes = fail(self, overlay, nodes, proposed_block)
|
root_votes = fail(self, overlay, nodes, proposed_block)
|
||||||
leader.propose_block(5, root_votes)
|
leader.propose_block(6, root_votes)
|
||||||
proposed_block = leader.latest_event
|
proposed_block = leader.latest_event
|
||||||
|
|
||||||
for view in range(6, 8):
|
for view in range(7, 8):
|
||||||
root_votes = succeed(self, overlay, nodes, proposed_block)
|
root_votes = succeed(self, overlay, nodes, proposed_block)
|
||||||
leader.propose_block(view, root_votes)
|
leader.propose_block(view, root_votes)
|
||||||
proposed_block = leader.latest_event
|
proposed_block = leader.latest_event
|
||||||
|
|
||||||
root_votes = fail(self, overlay, nodes, proposed_block)
|
root_votes = fail(self, overlay, nodes, proposed_block)
|
||||||
leader.propose_block(8, root_votes)
|
leader.propose_block(9, root_votes)
|
||||||
proposed_block = leader.latest_event
|
proposed_block = leader.latest_event
|
||||||
|
|
||||||
for view in range(9, 15):
|
for view in range(10, 15):
|
||||||
root_votes = succeed(self, overlay, nodes, proposed_block)
|
root_votes = succeed(self, overlay, nodes, proposed_block)
|
||||||
leader.propose_block(view, root_votes)
|
leader.propose_block(view, root_votes)
|
||||||
proposed_block = leader.latest_event
|
proposed_block = leader.latest_event
|
||||||
|
|
||||||
committed_blocks = [view for view in range(1, 11) if view not in (4, 7)]
|
committed_blocks = [view for view in range(1, 11) if view not in (4, 5, 7, 8)]
|
||||||
for node in nodes.values():
|
for node in nodes.values():
|
||||||
for view in committed_blocks:
|
for view in committed_blocks:
|
||||||
self.assertIn(view, [block.view for block in node.committed_blocks().values()])
|
self.assertIn(view, [block.view for block in node.committed_blocks().values()])
|
||||||
|
Loading…
x
Reference in New Issue
Block a user