2023-05-05 07:58:58 +00:00
|
|
|
# The Carnot protocol is designed to be elastic, responsive, and provide fast finality
|
|
|
|
# Elastic scalability allows the protocol to operate effectively with both small and large networks
|
|
|
|
# All nodes in the Carnot network participate in the consensus of a block
|
|
|
|
# Optimistic responsiveness enables the protocol to operate quickly during periods of synchrony and honest leadership
|
|
|
|
# There is no block generation time in Carnot, allowing for fast finality
|
|
|
|
# Carnot avoids the chain reorg problem, making it compatible with PoS schemes
|
|
|
|
# This enhances the robustness of the protocol, making it a valuable addition to the ecosystem of consensus protocols
|
|
|
|
|
|
|
|
|
|
|
|
# The protocol in Carnot operates in two modes: the happy path and the unhappy path.
|
|
|
|
#
|
|
|
|
# In Carnot, nodes are arranged in a binary tree overlay committee structure. Moreover, Carnot is a
|
|
|
|
# pipelined consensus protocol where a block contains the proof of attestation of its parent. In happy path the
|
|
|
|
# leader proposes a block that contains a quorum certificate (QC) with votes from more than two-thirds of the root
|
|
|
|
# committee and its child committee/ committees. The voting process begins at the leaf committee where nodes verify
|
|
|
|
# the proposal and send their votes to the parent committee. Once a node in the parent committee receives more than
|
|
|
|
# two-thirds of the votes from its child committee members, it sends its votes to its parent. This process continues
|
|
|
|
# recursively until the root committee members collect votes from its child committee/ committees. The root committee
|
|
|
|
# member builds a QC from the votes and sends it to the next leader. The leader builds a QC and proposes the next block
|
|
|
|
# upon receiving more than two-thirds of votes.
|
|
|
|
|
|
|
|
|
|
|
|
# In the unhappy path, if a node does not receive a message within a timeout interval, it will timeout. Only nodes at
|
|
|
|
# the root committee and its child committee/ committees send their timeout messages to the root committee. The root
|
|
|
|
# committee builds a timeout QC from more than two-thirds of messages, recalculates the new overlay, and broadcasts it
|
|
|
|
# to the network. Similar to the happy path, the timeout message moves from leaves to the root. Each parent waits for
|
|
|
|
# more than two-thirds of timeout messages from its child committees and sends its timeout to the parent committee once
|
|
|
|
# the threshold is reached. A node in the root committee builds a QC from timeout messages received from its
|
|
|
|
# child committee/committees and forwards it to the next leader. Upon receiving more than two-thirds of timeout
|
|
|
|
# messages, the next leader builds an aggregated QC and proposes the next block containing the aggregated QC.
|
|
|
|
# It should be noted that while receiving timeout messages, each node also updates its high_qc (the most recent QC)
|
|
|
|
# and passes it to its parent through the timeout message. In this way, the aggregated QC will include the high_qc seen
|
|
|
|
# by the majority of honest nodes. Hence, after the view change, the protocol safety is preserved.
|
|
|
|
|
|
|
|
|
|
|
|
# Please note this is still a work in progress
|
|
|
|
|
|
|
|
from dataclasses import dataclass
|
2023-05-18 16:29:28 +00:00
|
|
|
from typing import TypeAlias, List, Set, Self, Optional, Dict
|
|
|
|
from abc import abstractmethod, ABC
|
2023-05-05 07:58:58 +00:00
|
|
|
|
|
|
|
Id: TypeAlias = bytes
|
|
|
|
View: TypeAlias = int
|
|
|
|
Committee: TypeAlias = Set[Id]
|
|
|
|
|
|
|
|
|
|
|
|
def int_to_id(i: int) -> Id:
|
|
|
|
return bytes(str(i), encoding="utf8")
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass(unsafe_hash=True)
|
|
|
|
class StandardQc:
|
|
|
|
block: Id
|
|
|
|
view: View
|
|
|
|
|
|
|
|
def view(self) -> View:
|
|
|
|
return self.view
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
class AggregateQc:
|
|
|
|
qcs: List[View]
|
|
|
|
highest_qc: StandardQc
|
|
|
|
view: View
|
|
|
|
|
|
|
|
def view(self) -> View:
|
|
|
|
return self.view
|
|
|
|
|
|
|
|
def high_qc(self) -> StandardQc:
|
|
|
|
assert self.highest_qc.view == max(self.qcs)
|
|
|
|
return self.highest_qc
|
|
|
|
|
|
|
|
|
|
|
|
Qc: TypeAlias = StandardQc | AggregateQc
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
class Block:
|
|
|
|
view: View
|
|
|
|
qc: Qc
|
|
|
|
_id: Id # this is an abstration over the block id, which should be the hash of the contents
|
|
|
|
|
|
|
|
def extends(self, ancestor: Self) -> bool:
|
|
|
|
"""
|
|
|
|
:param ancestor:
|
|
|
|
:return: true if block is descendant of the ancestor in the chain
|
|
|
|
"""
|
|
|
|
return self.view > ancestor.view
|
|
|
|
|
|
|
|
def parent(self) -> Id:
|
|
|
|
match self.qc:
|
|
|
|
case StandardQc(block):
|
|
|
|
return block
|
|
|
|
case AggregateQc() as aqc:
|
|
|
|
return aqc.high_qc().block
|
|
|
|
|
|
|
|
def id(self) -> Id:
|
|
|
|
return self._id
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass(unsafe_hash=True)
|
|
|
|
class Vote:
|
|
|
|
block: Id
|
|
|
|
view: View
|
|
|
|
voter: Id
|
|
|
|
qc: Optional[Qc]
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
class TimeoutQc:
|
|
|
|
view: View
|
|
|
|
high_qc: Qc
|
|
|
|
qc_views: List[View]
|
|
|
|
sender_ids: Set[Id]
|
|
|
|
sender: Id
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
class Timeout:
|
|
|
|
"""
|
|
|
|
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.
|
|
|
|
"""
|
|
|
|
view: View
|
|
|
|
high_qc: Qc
|
|
|
|
sender: Id
|
|
|
|
timeout_qc: TimeoutQc
|
|
|
|
|
|
|
|
|
|
|
|
# Timeout has been detected, nodes agree on it and gather high qc
|
|
|
|
@dataclass
|
|
|
|
class NewView:
|
|
|
|
view: View
|
|
|
|
high_qc: Qc
|
|
|
|
sender: Id
|
|
|
|
timeout_qc: TimeoutQc
|
|
|
|
|
|
|
|
|
|
|
|
Quorum: TypeAlias = Set[Vote] | Set[NewView]
|
|
|
|
|
|
|
|
|
|
|
|
Payload: TypeAlias = Block | Vote | Timeout | NewView | TimeoutQc
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
class BroadCast:
|
|
|
|
payload: Payload
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
class Send:
|
|
|
|
to: [Id]
|
|
|
|
payload: Payload
|
|
|
|
|
|
|
|
|
|
|
|
Event: TypeAlias = BroadCast | Send
|
|
|
|
|
2023-05-18 16:29:28 +00:00
|
|
|
|
2023-05-05 07:58:58 +00:00
|
|
|
class Overlay:
|
|
|
|
"""
|
|
|
|
Overlay structure for a View
|
|
|
|
"""
|
|
|
|
|
|
|
|
@abstractmethod
|
|
|
|
def is_leader(self, _id: Id):
|
|
|
|
"""
|
|
|
|
:param _id: Node id to be checked
|
|
|
|
:return: true if node is the leader of the current view
|
|
|
|
"""
|
2023-05-18 16:29:28 +00:00
|
|
|
return _id == self.leader()
|
2023-05-05 07:58:58 +00:00
|
|
|
|
|
|
|
@abstractmethod
|
2023-05-18 16:29:28 +00:00
|
|
|
def leader(self) -> Id:
|
2023-05-05 07:58:58 +00:00
|
|
|
"""
|
|
|
|
:param view:
|
|
|
|
:return: the leader Id of the specified view
|
|
|
|
"""
|
|
|
|
pass
|
|
|
|
|
2023-05-18 16:29:28 +00:00
|
|
|
@abstractmethod
|
|
|
|
def next_leader(self) -> Id:
|
|
|
|
pass
|
|
|
|
|
2023-05-05 07:58:58 +00:00
|
|
|
@abstractmethod
|
|
|
|
def is_member_of_leaf_committee(self, _id: Id) -> bool:
|
|
|
|
"""
|
|
|
|
:param _id: Node id to be checked
|
|
|
|
:return: true if the participant with Id _id is in the leaf committee of the committee overlay
|
|
|
|
"""
|
|
|
|
pass
|
|
|
|
|
|
|
|
@abstractmethod
|
|
|
|
def is_member_of_root_committee(self, _id: Id) -> bool:
|
|
|
|
"""
|
|
|
|
:param _id:
|
|
|
|
:return: true if the participant with Id _id is member of the root committee withing the tree overlay
|
|
|
|
"""
|
|
|
|
pass
|
|
|
|
|
|
|
|
@abstractmethod
|
|
|
|
def is_member_of_child_committee(self, parent: Id, child: Id) -> bool:
|
|
|
|
"""
|
|
|
|
:param parent:
|
|
|
|
:param child:
|
|
|
|
:return: true if participant with Id child is member of the child committee of the participant with Id parent
|
|
|
|
"""
|
|
|
|
pass
|
|
|
|
|
|
|
|
@abstractmethod
|
|
|
|
def parent_committee(self, _id: Id) -> Optional[Committee]:
|
|
|
|
"""
|
|
|
|
:param _id:
|
|
|
|
:return: Some(parent committee) of the participant with Id _id withing the committee tree overlay
|
|
|
|
or Empty if the member with Id _id is a participant of the root committee
|
|
|
|
"""
|
|
|
|
pass
|
|
|
|
|
|
|
|
@abstractmethod
|
|
|
|
def leaf_committees(self) -> Set[Committee]:
|
|
|
|
pass
|
|
|
|
|
|
|
|
@abstractmethod
|
|
|
|
def root_committee(self) -> Committee:
|
|
|
|
"""
|
|
|
|
:return: returns root committee
|
|
|
|
"""
|
|
|
|
pass
|
|
|
|
|
|
|
|
@abstractmethod
|
|
|
|
def is_child_of_root_committee(self, _id: Id) -> bool:
|
|
|
|
"""
|
|
|
|
:return: returns child committee/s of root committee if present
|
|
|
|
"""
|
|
|
|
pass
|
|
|
|
|
|
|
|
@abstractmethod
|
|
|
|
def leader_super_majority_threshold(self, _id: Id) -> int:
|
|
|
|
"""
|
|
|
|
Amount of distinct number of messages for a node with Id _id member of a committee
|
|
|
|
The return value may change depending on which committee the node is member of, including the leader
|
|
|
|
:return:
|
|
|
|
"""
|
|
|
|
pass
|
|
|
|
|
|
|
|
@abstractmethod
|
|
|
|
def super_majority_threshold(self, _id: Id) -> int:
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
def download(view) -> Block:
|
|
|
|
raise NotImplementedError
|
|
|
|
|
|
|
|
|
|
|
|
class Carnot:
|
2023-05-18 16:29:28 +00:00
|
|
|
def __init__(self, _id: Id, overlay=Overlay()):
|
2023-05-05 07:58:58 +00:00
|
|
|
self.id: Id = _id
|
|
|
|
# Current View counter
|
|
|
|
# It is the view currently being processed by the node. Once a Qc is received, the view is considered completed
|
|
|
|
# and the current view is updated to qc.view+1
|
|
|
|
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 = -1
|
|
|
|
# This is most recent (in terms of view) Standard QC that has been received by the node
|
|
|
|
self.local_high_qc: Optional[Qc] = None
|
|
|
|
# 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()
|
2023-05-18 16:29:28 +00:00
|
|
|
# Whether the node time out in the last view and corresponding qc
|
2023-05-05 07:58:58 +00:00
|
|
|
self.last_view_timeout_qc: Optional[TimeoutQc] = None
|
2023-05-18 16:29:28 +00:00
|
|
|
self.overlay: Overlay = overlay
|
2023-05-05 07:58:58 +00:00
|
|
|
|
|
|
|
|
|
|
|
# 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:
|
|
|
|
return (
|
|
|
|
block.view >= self.current_view and
|
|
|
|
block.view == block.qc.view + 1
|
|
|
|
)
|
|
|
|
|
|
|
|
# Ask Dani
|
|
|
|
def update_high_qc(self, qc: Qc):
|
|
|
|
match (self.local_high_qc, qc):
|
|
|
|
case (None, StandardQc() as new_qc):
|
|
|
|
self.local_high_qc = new_qc
|
|
|
|
case (None, AggregateQc() as new_qc):
|
|
|
|
self.local_high_qc = new_qc.high_qc()
|
|
|
|
case (old_qc, StandardQc() as new_qc) if new_qc.view > old_qc.view:
|
|
|
|
self.local_high_qc = new_qc
|
|
|
|
case (old_qc, AggregateQc() as new_qc) if new_qc.high_qc().view != old_qc.view:
|
|
|
|
self.local_high_qc = new_qc.high_qc()
|
|
|
|
# if my view is not updated I update it when I see a qc for that view
|
|
|
|
if qc.view == self.current_view:
|
|
|
|
self.current_view = self.current_view + 1
|
|
|
|
|
|
|
|
def update_timeout_qc(self, timeout_qc: TimeoutQc):
|
|
|
|
match (self.last_view_timeout_qc, timeout_qc):
|
|
|
|
case (None, timeout_qc):
|
|
|
|
self.last_view_timeout_qc = timeout_qc
|
|
|
|
case (self.last_view_timeout_qc, timeout_qc) if timeout_qc.view > self.last_view_timeout_qc.view:
|
|
|
|
self.last_view_timeout_qc = timeout_qc
|
|
|
|
|
|
|
|
def receive_block(self, block: Block):
|
|
|
|
assert block.parent() in self.safe_blocks
|
|
|
|
|
|
|
|
if block.id() in self.safe_blocks:
|
|
|
|
return
|
|
|
|
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
|
|
|
|
|
|
|
|
# TODO: check the proposer of the block is indeed leader for that view
|
|
|
|
|
|
|
|
if self.block_is_safe(block):
|
|
|
|
self.safe_blocks[block.id()] = block
|
|
|
|
self.update_high_qc(block.qc)
|
|
|
|
|
|
|
|
def approve_block(self, block: Block, votes: Set[Vote]) -> Event:
|
|
|
|
assert block.id() in self.safe_blocks
|
|
|
|
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(vote.block == block.id() for vote in votes)
|
|
|
|
assert self.highest_voted_view < block.view
|
|
|
|
|
|
|
|
if self.overlay.is_member_of_root_committee(self.id):
|
|
|
|
qc = self.build_qc(block.view, block, None)
|
|
|
|
else:
|
|
|
|
qc = None
|
|
|
|
|
|
|
|
vote: Vote = Vote(
|
|
|
|
block=block.id(),
|
|
|
|
voter=self.id,
|
|
|
|
view=block.view,
|
|
|
|
qc=qc
|
|
|
|
)
|
|
|
|
|
|
|
|
self.highest_voted_view = max(self.highest_voted_view, block.view)
|
|
|
|
|
|
|
|
if self.overlay.is_member_of_root_committee(self.id):
|
|
|
|
return Send(to=self.overlay.leader(block.view + 1), payload=vote)
|
|
|
|
return Send(to=self.overlay.parent_committee(self.id), payload=vote)
|
|
|
|
|
|
|
|
def forward_vote(self, vote: Vote) -> Optional[Event]:
|
2023-11-02 09:40:30 +00:00
|
|
|
"""
|
|
|
|
Recall that for the leader to propose a block it's necessary to have a threshold of the top three committees
|
|
|
|
votes. In case part of the root committee is malicious and does not vote, the leader might not reach this threshold
|
|
|
|
even though more than 2/3 of the total nodes voted in favor if nodes stop sending votes after the threshold.
|
|
|
|
By forwarding votes from children (only the root committee does this), we can improve liveness in this case.
|
|
|
|
"""
|
2023-05-05 07:58:58 +00:00
|
|
|
assert vote.block in self.safe_blocks
|
|
|
|
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):
|
2023-05-18 16:29:28 +00:00
|
|
|
return Send(to=self.overlay.next_leader(), payload=vote)
|
2023-05-05 07:58:58 +00:00
|
|
|
|
|
|
|
def forward_new_view(self, msg: NewView) -> Optional[Event]:
|
|
|
|
assert msg.view == self.current_view
|
|
|
|
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 == msg.view
|
|
|
|
|
|
|
|
if self.overlay.is_member_of_root_committee(self.id):
|
2023-05-18 16:29:28 +00:00
|
|
|
return Send(to=self.overlay.next_leader(), payload=msg)
|
2023-05-05 07:58:58 +00:00
|
|
|
|
|
|
|
def build_qc(self, view: View, block: Optional[Block], new_views: Optional[Set[NewView]]) -> Qc:
|
|
|
|
# unhappy path
|
|
|
|
if new_views:
|
|
|
|
new_views = list(new_views)
|
|
|
|
return AggregateQc(
|
|
|
|
qcs=[msg.high_qc.view for msg in new_views],
|
|
|
|
highest_qc=max(new_views, key=lambda x: x.high_qc.view).high_qc,
|
|
|
|
view=new_views[0].view
|
|
|
|
)
|
|
|
|
# happy path
|
|
|
|
return StandardQc(
|
|
|
|
view=view,
|
|
|
|
block=block.id()
|
|
|
|
)
|
|
|
|
|
|
|
|
def propose_block(self, view: View, quorum: Quorum) -> Event:
|
|
|
|
assert self.overlay.is_leader(self.id)
|
|
|
|
assert len(quorum) >= self.overlay.leader_super_majority_threshold(self.id)
|
|
|
|
|
|
|
|
qc = None
|
|
|
|
quorum = list(quorum)
|
|
|
|
# happy path
|
|
|
|
if isinstance(quorum[0], Vote):
|
|
|
|
vote = quorum[0]
|
|
|
|
qc = self.build_qc(vote.view, self.safe_blocks[vote.block], None)
|
|
|
|
# unhappy path
|
|
|
|
elif isinstance(quorum[0], NewView):
|
|
|
|
new_view = quorum[0]
|
|
|
|
qc = self.build_qc(new_view.view, None, quorum)
|
|
|
|
|
|
|
|
block = Block(
|
|
|
|
view=view,
|
|
|
|
qc=qc,
|
|
|
|
# Dummy id for proposing next block
|
|
|
|
_id=int_to_id(hash(
|
|
|
|
(
|
|
|
|
bytes(f"{view}".encode(encoding="utf8")),
|
|
|
|
bytes(f"{qc.view}".encode(encoding="utf8"))
|
|
|
|
)
|
|
|
|
))
|
|
|
|
)
|
|
|
|
return BroadCast(payload=block)
|
|
|
|
|
|
|
|
def is_safe_to_timeout_invariant(
|
|
|
|
self,
|
|
|
|
):
|
|
|
|
"""
|
|
|
|
Local timeout is different for the root and its child committees. If other committees timeout, they only
|
|
|
|
stop taking part in consensus. If a member of root or its child committees timeout it sends its timeout message
|
|
|
|
to all members of root to build the timeout qc. Using this qc we assume that the new
|
|
|
|
overlay can be built. Hence, by building the new overlay members of root committee can send the timeout qc
|
|
|
|
to the leaf committee of the new overlay. Upon receipt of the timeout qc the leaf committee members update
|
|
|
|
their local_high_qc, last_timeout_view_qc and last_voted_view if the view of qcs
|
|
|
|
(local_high_qc, last_timeout_view_qc) received is higher than their local view. Similarly last_voted_view is
|
|
|
|
updated if it is greater than the current last_voted_view. When parent committee member receives more than two
|
|
|
|
third of timeout messages from its children it also updates its local_high_qc, last_timeout_view_qc and
|
|
|
|
last_voted_view if needed and then send its timeout message upward. In this way the latest qcs move upward
|
|
|
|
that makes it possible for the next leader to propose a block with the latest local_high_qcs in aggregated qc
|
|
|
|
from more than two third members of root committee and its children.
|
|
|
|
"""
|
|
|
|
|
|
|
|
# Make sure the node doesn't time out continuously without finishing the step to increment the current view.
|
|
|
|
# Make sure current view is always higher than the local_high_qc so that the node won't timeout unnecessary
|
|
|
|
# for a previous view.
|
|
|
|
assert self.current_view > max(self.highest_voted_view - 1, self.local_high_qc.view)
|
|
|
|
# This condition makes sure a node waits for timeout_qc from root committee to change increment its view with
|
|
|
|
# a view change.
|
|
|
|
# A node must change its view after making sure it has the high_Qc or last_timeout_view_qc
|
|
|
|
# from previous view.
|
|
|
|
return (
|
2023-05-18 16:29:28 +00:00
|
|
|
self.current_view == self.local_high_qc.view + 1 or
|
2023-05-05 07:58:58 +00:00
|
|
|
self.current_view == self.last_view_timeout_qc.view + 1 or
|
|
|
|
(self.current_view == self.last_view_timeout_qc.view)
|
|
|
|
)
|
|
|
|
|
|
|
|
def local_timeout(self) -> Optional[Event]:
|
|
|
|
"""
|
|
|
|
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):
|
|
|
|
timeout_msg: Timeout = Timeout(
|
|
|
|
view=self.current_view,
|
|
|
|
high_qc=self.local_high_qc,
|
|
|
|
# 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_view_timeout_qc,
|
|
|
|
sender=self.id
|
|
|
|
)
|
|
|
|
return Send(payload=timeout_msg, to=self.overlay.root_committee())
|
|
|
|
|
|
|
|
def timeout_detected(self, msgs: Set[Timeout]) -> Event:
|
|
|
|
"""
|
|
|
|
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
|
|
|
|
necessary to reconstruct the new overlay
|
|
|
|
|
|
|
|
"""
|
|
|
|
assert len(msgs) == self.overlay.leader_super_majority_threshold(self.id)
|
|
|
|
assert all(msg.view >= self.current_view for msg in msgs)
|
|
|
|
assert len(set(msg.view for msg in msgs)) == 1
|
|
|
|
assert self.overlay.is_member_of_root_committee(self.id)
|
|
|
|
|
|
|
|
timeout_qc = self.build_timeout_qc(msgs, self.id)
|
|
|
|
return BroadCast(payload=timeout_qc) # we broadcast so all nodes can get ready for voting on a new view
|
|
|
|
# Note that receive_timeout qc should be called for root nodes as well
|
|
|
|
|
|
|
|
# noinspection PyTypeChecker
|
|
|
|
def approve_new_view(self, timeout_qc: TimeoutQc, new_views: Set[NewView]) -> Event:
|
|
|
|
"""
|
|
|
|
We will always need for timeout_qc to have been preprocessed by the received_timeout_qc method when the event
|
|
|
|
happens before approve_new_view is processed.
|
|
|
|
"""
|
|
|
|
# newView.view == self.last_timeout_view_qc.view for member of root committee and its children because
|
|
|
|
# they have already created the timeout_qc. For other nodes newView.view > self.last_timeout_view_qc.view.
|
|
|
|
if self.last_view_timeout_qc is not None:
|
|
|
|
assert all(new_view.view > self.last_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 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
|
|
|
|
messages_high_qc = (new_view.high_qc for new_view in new_views)
|
|
|
|
high_qc = max(
|
|
|
|
[timeout_qc.high_qc, *messages_high_qc],
|
|
|
|
key=lambda qc: qc.view
|
|
|
|
)
|
|
|
|
self.update_high_qc(high_qc)
|
|
|
|
timeout_msg = NewView(
|
|
|
|
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,
|
|
|
|
sender=self.id,
|
|
|
|
timeout_qc=timeout_qc,
|
|
|
|
)
|
|
|
|
|
|
|
|
# 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.
|
|
|
|
self.highest_voted_view = max(self.highest_voted_view, view)
|
|
|
|
|
|
|
|
if self.overlay.is_member_of_root_committee(self.id):
|
2023-05-18 16:29:28 +00:00
|
|
|
return Send(payload=timeout_msg, to=[self.overlay.next_leader()])
|
2023-05-05 07:58:58 +00:00
|
|
|
return Send(payload=timeout_msg, to=self.overlay.parent_committee(self.id))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Just a suggestion that received_timeout_qc can be reused by each node when the process timeout_qc of the NewView msg.
|
|
|
|
# TODO: check that receiving (and processing) a timeout qc "in the future" allows to process old(er) blocks
|
|
|
|
# e.g. we might still need access to the old leader schedule to validate qcs
|
|
|
|
def receive_timeout_qc(self, timeout_qc: TimeoutQc):
|
|
|
|
if timeout_qc.view < self.current_view:
|
|
|
|
return
|
|
|
|
new_high_qc = timeout_qc.high_qc
|
|
|
|
self.update_high_qc(new_high_qc)
|
|
|
|
self.update_timeout_qc(timeout_qc)
|
|
|
|
# Update our current view and go ahead with the next step
|
|
|
|
self.update_current_view_from_timeout_qc(timeout_qc)
|
2023-05-18 16:29:28 +00:00
|
|
|
# self.rebuild_overlay_from_timeout_qc(timeout_qc)
|
2023-05-05 07:58:58 +00:00
|
|
|
|
|
|
|
def rebuild_overlay_from_timeout_qc(self, timeout_qc: TimeoutQc):
|
|
|
|
assert timeout_qc.view >= self.current_view
|
|
|
|
self.overlay = Overlay()
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def build_timeout_qc(msgs: Set[Timeout], sender: Id) -> TimeoutQc:
|
|
|
|
msgs = list(msgs)
|
|
|
|
return TimeoutQc(
|
|
|
|
view=msgs[0].view,
|
|
|
|
high_qc=max(msgs, key=lambda x: x.high_qc.view).high_qc,
|
|
|
|
qc_views=[msg.view for msg in msgs],
|
|
|
|
sender_ids={msg.sender for msg in msgs},
|
|
|
|
sender=sender,
|
|
|
|
)
|
|
|
|
|
|
|
|
def update_current_view_from_timeout_qc(self, timeout_qc: TimeoutQc):
|
|
|
|
self.current_view = timeout_qc.view + 1
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
pass
|