mirror of
https://github.com/logos-blockchain/logos-blockchain-specs.git
synced 2026-01-05 06:33:08 +00:00
Extract implicit information from safe blocks (#19)
* extract implicit information from safe blocks * fix test
This commit is contained in:
parent
a291370096
commit
c0eba7d8e8
120
carnot/carnot.py
120
carnot/carnot.py
@ -230,10 +230,6 @@ def download(view) -> Block:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def is_sequential_ascending(view1: View, view2: View):
|
||||
return view1 == view2 + 1
|
||||
|
||||
|
||||
class Carnot:
|
||||
def __init__(self, _id: Id):
|
||||
self.id: Id = _id
|
||||
@ -241,37 +237,78 @@ class Carnot:
|
||||
self.current_view: View = 0
|
||||
# Highest voted view counter. This is used to prevent a node from voting twice or vote after timeout.
|
||||
self.highest_voted_view: View = 0
|
||||
# This is the qc from the highest view a node has
|
||||
# This is most recent (in terms of view) Standard QC that has been received by the node
|
||||
self.local_high_qc: Optional[Qc] = None
|
||||
# The latest view committed by a node.
|
||||
self.latest_committed_view: View = 0
|
||||
# Validated blocks with their validated QCs are included here. If commit conditions is satisfied for
|
||||
# Validated blocks with their validated QCs are included here. If commit conditions are satisfied for
|
||||
# each one of these blocks it will be committed.
|
||||
self.safe_blocks: Dict[Id, Block] = dict()
|
||||
# Block received for a specific view. Make sure the node doesn't receive duplicate blocks.
|
||||
self.seen_view_blocks: Dict[View, bool] = dict()
|
||||
# Last timeout QC and its view
|
||||
# Whether the node timeed out in the last view and corresponding qc
|
||||
self.last_timeout_view_qc: Optional[TimeoutQc] = None
|
||||
self.last_timeout_view: Optional[View] = None
|
||||
self.overlay: Overlay = Overlay() # TODO: integrate overlay
|
||||
# Committed blocks are kept here.
|
||||
self.committed_blocks: Dict[Id, Block] = dict()
|
||||
|
||||
|
||||
# Committing conditions for a block
|
||||
# TODO: explain the conditions in comment
|
||||
def can_commit_grandparent(self, block) -> bool:
|
||||
parent = self.safe_blocks.get(block.parent())
|
||||
grand_parent = self.safe_blocks.get(parent.parent())
|
||||
# this case should just trigger on genesis_case,
|
||||
# as the preconditions on outer calls should check on block validity
|
||||
if not parent or not grand_parent:
|
||||
return False
|
||||
return (
|
||||
parent.view == (grand_parent.view + 1) and
|
||||
isinstance(block.qc, (StandardQc,)) and
|
||||
isinstance(parent.qc, (StandardQc,))
|
||||
)
|
||||
|
||||
|
||||
# The latest committed view is implicit in the safe blocks tree given
|
||||
# the committing conditions.
|
||||
# For convenience, this is an helper method to retrieve that value.
|
||||
def latest_committed_view(self) -> View:
|
||||
return self.latest_committed_block().view
|
||||
|
||||
# Return the list of blocks received by a node for a specific view.
|
||||
# It will return more than one block only in case of a malicious leader
|
||||
def blocks_in_view(self, view: View) -> List[Block]:
|
||||
return [block for block in self.safe_blocks.values() if block.view == view]
|
||||
|
||||
def genesis_block(self) -> Block:
|
||||
return self.blocks_in_view(0)[0]
|
||||
|
||||
def latest_committed_block(self) -> Block:
|
||||
for view in range(self.current_view, 0, -1):
|
||||
for block in self.blocks_in_view(view):
|
||||
if self.can_commit_grandparent(block):
|
||||
return self.safe_blocks.get(self.safe_blocks.get(block.parent()).parent())
|
||||
# genesis blocks is always considered committed
|
||||
return self.genesis_block()
|
||||
|
||||
# Given committing conditions, the set of committed blocks is implicit
|
||||
# in the safe blocks tree. For convenience, this is an helper method to
|
||||
# retrieve that set.
|
||||
def committed_blocks(self) -> Dict[Id, Block]:
|
||||
tip = self.latest_committed_block()
|
||||
committed_blocks = {tip.id(): tip, self.genesis_block().id: self.genesis_block()}
|
||||
while tip.view > 0:
|
||||
committed_blocks[tip.id()] = tip
|
||||
tip = self.safe_blocks.get(tip.parent())
|
||||
return committed_blocks
|
||||
|
||||
def block_is_safe(self, block: Block) -> bool:
|
||||
match block.qc:
|
||||
case StandardQc() as standard:
|
||||
if standard.view < self.latest_committed_view:
|
||||
return False
|
||||
return (
|
||||
block.view >= self.latest_committed_view and
|
||||
is_sequential_ascending(block.view, standard.view)
|
||||
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:
|
||||
if aggregated.high_qc().view < self.latest_committed_view():
|
||||
return False
|
||||
return (
|
||||
block.view >= self.current_view and
|
||||
is_sequential_ascending(block.view, aggregated.view)
|
||||
block.view == aggregated.view + 1
|
||||
)
|
||||
|
||||
# Ask Dani
|
||||
@ -301,15 +338,17 @@ class Carnot:
|
||||
|
||||
if block.id() in self.safe_blocks:
|
||||
return
|
||||
if self.seen_view_blocks.get(block.view) is not None or block.view <= self.latest_committed_view:
|
||||
if self.blocks_in_view(block.view) != [] or block.view <= self.latest_committed_view():
|
||||
# TODO: Report malicious leader
|
||||
# TODO: it could be possible that a malicious leader send a block to a node and another one to
|
||||
# the rest of the network. The node should be able to catch up with the rest of the network after having
|
||||
# validated that the history of the block is correct and diverged from its fork.
|
||||
# By rejecting any other blocks except the first one received for a view this code does NOT do that.
|
||||
return
|
||||
|
||||
if self.block_is_safe(block):
|
||||
self.safe_blocks[block.id()] = block
|
||||
self.seen_view_blocks[block.view] = True
|
||||
self.update_high_qc(block.qc)
|
||||
self.try_commit_grand_parent(block)
|
||||
|
||||
def approve_block(self, block: Block, votes: Set[Vote]):
|
||||
assert block.id() in self.safe_blocks
|
||||
@ -424,8 +463,8 @@ class Carnot:
|
||||
# A node must change its view after making sure it has the high_Qc or last_timeout_view_qc
|
||||
# from previous view.
|
||||
return (
|
||||
is_sequential_ascending(self.current_view, self.local_high_qc.view) or
|
||||
is_sequential_ascending(self.current_view, self.last_timeout_view_qc.view) or
|
||||
self.current_view == self.local_high_qc.view + 1 or
|
||||
self.current_view == self.last_timeout_view_qc.view + 1 or
|
||||
(self.current_view == self.last_timeout_view_qc.view)
|
||||
)
|
||||
|
||||
@ -500,7 +539,7 @@ class Carnot:
|
||||
self.increment_voted_view(timeout_qc.view)
|
||||
|
||||
# Just a suggestion that received_timeout_qc can be reused by each node when the process timeout_qc of the NewView msg.
|
||||
def received_timeout_qc(self, timeout_qc: TimeoutQc):
|
||||
def receive_timeout_qc(self, timeout_qc: TimeoutQc):
|
||||
# assert timeout_qc.view >= self.current_view
|
||||
new_high_qc = timeout_qc.high_qc
|
||||
if new_high_qc.view > self.local_high_qc.view:
|
||||
@ -529,32 +568,9 @@ class Carnot:
|
||||
def broadcast(self, block):
|
||||
pass
|
||||
|
||||
# todo blocks from latest_committed_block to grand_parent must be committed.
|
||||
def try_commit_grand_parent(self, block: Block):
|
||||
|
||||
parent = self.safe_blocks.get(block.parent())
|
||||
grand_parent = self.safe_blocks.get(parent.parent())
|
||||
# this case should just trigger on genesis_case,
|
||||
# as the preconditions on outer calls should check on block validity
|
||||
if not parent or not grand_parent:
|
||||
return
|
||||
can_commit = (
|
||||
parent.view == (grand_parent.view + 1) and
|
||||
isinstance(block.qc, (StandardQc,)) and
|
||||
isinstance(parent.qc, (StandardQc,))
|
||||
)
|
||||
|
||||
while can_commit and grand_parent and grand_parent.id() not in self.committed_blocks:
|
||||
self.committed_blocks[grand_parent.id()] = grand_parent
|
||||
self.increment_latest_committed_view(grand_parent.view)
|
||||
grand_parent = self.safe_blocks.get(grand_parent.parent())
|
||||
|
||||
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 reset_last_timeout_view_qc(self, qc: Qc):
|
||||
if qc.view < self.current_view:
|
||||
return
|
||||
@ -565,12 +581,6 @@ class Carnot:
|
||||
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]) -> Timeout:
|
||||
assert len(timeouts) > 0
|
||||
return max(timeouts, key=lambda time: time.qc.view)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@ -20,7 +20,6 @@ class TestCarnotHappyPath(TestCase):
|
||||
def add_genesis_block(carnot: Carnot) -> Block:
|
||||
genesis_block = Block(view=0, qc=StandardQc(block=b"", view=0), _id=b"")
|
||||
carnot.safe_blocks[genesis_block.id()] = genesis_block
|
||||
carnot.committed_blocks[genesis_block.id()] = genesis_block
|
||||
return genesis_block
|
||||
|
||||
def test_receive_block(self):
|
||||
@ -134,9 +133,8 @@ class TestCarnotHappyPath(TestCase):
|
||||
|
||||
block5 = Block(view=5, qc=StandardQc(block=block4.id(), view=4), _id=b"5")
|
||||
carnot.receive_block(block5)
|
||||
|
||||
for block in (block1, block2, block3):
|
||||
self.assertIn(block.id(), carnot.committed_blocks)
|
||||
self.assertIn(block.id(), carnot.committed_blocks())
|
||||
|
||||
def test_receive_block_has_an_old_qc_and_tries_to_revert_a_committed_block(self):
|
||||
"""
|
||||
@ -190,7 +188,7 @@ class TestCarnotHappyPath(TestCase):
|
||||
self.assertEqual(len(carnot.safe_blocks), 5)
|
||||
block5 = Block(view=5, qc=StandardQc(block=block4.id(), view=4), _id=b"5")
|
||||
carnot.receive_block(block5)
|
||||
self.assertEqual(carnot.latest_committed_view, 3)
|
||||
self.assertEqual(carnot.latest_committed_view(), 3)
|
||||
self.assertEqual(carnot.local_high_qc.view, 4)
|
||||
|
||||
# Test cases for vote:
|
||||
@ -230,8 +228,8 @@ class TestCarnotHappyPath(TestCase):
|
||||
carnot.approve_block(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)
|
||||
self.assertEqual(carnot.latest_committed_view(), 0)
|
||||
self.assertEqual(carnot.last_timeout_view_qc, None)
|
||||
|
||||
def test_vote_for_received_block_if_threshold_votes_has_not_reached(self):
|
||||
"""
|
||||
|
||||
@ -95,7 +95,6 @@ def add_genesis_block(carnot: Carnot) -> Block:
|
||||
carnot.increment_voted_view(0)
|
||||
carnot.local_high_qc = genesis_block.qc
|
||||
carnot.current_view = 1
|
||||
carnot.committed_blocks[genesis_block.id()] = genesis_block
|
||||
return genesis_block
|
||||
|
||||
|
||||
@ -181,7 +180,7 @@ def fail(test_case: TestCase, overlay: MockOverlay, nodes: Dict[Id, MockCarnot],
|
||||
timeout_qc = root_member.latest_event
|
||||
|
||||
for node in nodes.values():
|
||||
node.received_timeout_qc(timeout_qc)
|
||||
node.receive_timeout_qc(timeout_qc)
|
||||
|
||||
votes = {}
|
||||
childs_ids = list(chain.from_iterable(overlay.leaf_committees()))
|
||||
@ -234,7 +233,7 @@ class TestCarnotUnhappyPath(TestCase):
|
||||
self.assertEqual(leader.highest_voted_view, view)
|
||||
|
||||
for node in nodes.values():
|
||||
self.assertEqual(node.latest_committed_view, 0)
|
||||
self.assertEqual(node.latest_committed_view(), 0)
|
||||
|
||||
def test_interleave_success_fails(self):
|
||||
"""
|
||||
@ -272,4 +271,4 @@ class TestCarnotUnhappyPath(TestCase):
|
||||
committed_blocks = [view for view in range(1, 11) if view not in (4, 7)]
|
||||
for node in nodes.values():
|
||||
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