Consensus
Definitions and notation
For the purpose of maintaining consensus, transactions are grouped into blocks. There is a single preconfigured block called genesis block. Every block except has a link pointing to the previous block , where is the block, and is reachable from every block by following those links (that is, there are no cycles).
The links between blocks give rise to a partial order: for blocks and , means that and is reachable from by following links to previous blocks, and means that or . The relations and are defined as the reflected versions of and , respectively. Finally, means that either , or , and means the opposite.
A chain is a set of blocks reachable from block , which is called its tip. That is, . For any blocks and , there is a chain that both and belong to iff . In this case, and are said to be on the same chain.
Each block has an integer height . It is guaranteed that block heights are monotonic (that is, for any block , ), but they need not be consecutive. Also, may not be zero. Each node keeps track of a valid block with the largest height it knows about, which is called its head.
Blocks are grouped into epochs. In a chain, the set of blocks that belongs to some epoch forms a contiguous range: if blocks and such that belong to the same epoch, then every block such that also belongs to that epoch. Epochs can be identified by sequential indices: belongs to an epoch with index , and for every other block , the index of its epoch is either the same as that of , or one greater.
Each epoch is associated with a set of block producers that are validating blocks in that epoch, as well as an assignment of block heights to block producers that are responsible for producing a block at that height. A block producer responsible for producing a block at height is called block proposer at . This information (the set and the assignment) for an epoch with index is determined by the last block of the epoch with index . For epochs with indices and , this information is preconfigured. Therefore, if two chains share the last block of some epoch, they will have the same set and the same assignment for the next two epochs, but not necessarily for any epoch after that.
The consensus protocol defines a notion of finality. Informally, if a block is final, any future final blocks may only be built on top of . Therefore, transactions in and preceding blocks are never going to be reversed. Finality is not a function of a block itself, rather, a block may be final or not final in some chain it is a member of. Specifically, , where , means that is final in . A block that is final in a chain is final in all of its extensions: specifically, if is true, then is also true for all .
Data structures
The fields in the Block header relevant to the consensus process are:
struct BlockHeader {
...
prev_hash: BlockHash,
height: BlockHeight,
epoch_id: EpochId,
last_final_block_hash: BlockHash,
approvals: Vec<Option<Signature>>
...
}
Block producers in the particular epoch exchange many kinds of messages. The two kinds that are relevant to the consensus are Blocks and Approvals. The approval contains the following fields:
enum ApprovalInner {
Endorsement(BlockHash),
Skip(BlockHeight),
}
struct Approval {
inner: ApprovalInner,
target_height: BlockHeight,
signature: Signature,
account_id: AccountId
}
Where the parameter of the Endorsement
is the hash of the approved block, the parameter of the Skip
is the height of the approved block, target_height
is the specific height at which the approval can be used (an approval with a particular target_height
can be only included in the approvals
of a block that has height = target_height
), account_id
is the account of the block producer who created the approval, and signature
is their signature on the tuple (inner, target_height)
.
Approvals Requirements
Every block except the genesis block must logically contain approvals of a form described in the next paragraph from block producers whose cumulative stake exceeds of the total stake in the current epoch, and in specific conditions described in section epoch switches also the approvals of the same form from block producers whose cumulative stake exceeds of the total stake in the next epoch.
The approvals logically included in the block must be an Endorsement
with the hash of if and only if , otherwise it must be a Skip
with the height of . See this section below for details on why the endorsements must contain the hash of the previous block, and skips must contain the height.
Note that since each approval that is logically stored in the block is the same for each block producer (except for the account_id
of the sender and the signature
), it is redundant to store the full approvals. Instead physically we only store the signatures of the approvals. The specific way they are stored is the following: we first fetch the ordered set of block producers from the current epoch. If the block is on the epoch boundary and also needs to include approvals from the next epoch (see epoch switches), we add new accounts from the new epoch
def get_accounts_for_block_ordered(h, prev_block):
cur_epoch = get_next_block_epoch(prev_block)
next_epoch = get_next_block_next_epoch(prev_block)
account_ids = get_epoch_block_producers_ordered(cur_epoch)
if next_block_needs_approvals_from_next_epoch(prev_block):
for account_id in get_epoch_block_producers_ordered(next_epoch):
if account_id not in account_ids:
account_ids.append(account_id)
return account_ids
The block then contains a vector of optional signatures of the same or smaller size than the resulting set of account_ids
, with each element being None
if the approval for such account is absent, or the signature on the approval message if it is present. It's easy to show that the actual approvals that were signed by the block producers can easily be reconstructed from the information available in the block, and thus the signatures can be verified. If the vector of signatures is shorter than the length of account_ids
, the remaining signatures are assumed to be None
.
Messages
On receipt of the approval message the participant just stores it in the collection of approval messages.
def on_approval(self, approval):
self.approvals.append(approval)
Whenever a participant receives a block, the operations relevant to the consensus include updating the head
and initiating a timer to start sending the approvals on the block to the block producers at the consecutive target_height
s. The timer delays depend on the height of the last final block, so that information is also persisted.
def on_block(self, block):
header = block.header
if header.height <= self.head_height:
return
last_final_block = store.get_block(header.last_final_block_hash)
self.head_height = header.height
self.head_hash = block.hash()
self.largest_final_height = last_final_block.height
self.timer_height = self.head_height + 1
self.timer_started = time.time()
self.endorsement_pending = True
The timer needs to be checked periodically, and contain the following logic:
def get_delay(n):
min(MAX_DELAY, MIN_DELAY + DELAY_STEP * (n-2))
def process_timer(self):
now = time.time()
skip_delay = get_delay(self.timer_height - self.largest_final_height)
if self.endorsement_pending and now > self.timer_started + ENDORSEMENT_DELAY:
if self.head_height >= self.largest_target_height:
self.largest_target_height = self.head_height + 1
self.send_approval(head_height + 1)
self.endorsement_pending = False
if now > self.timer_started + skip_delay:
assert not self.endorsement_pending
self.largest_target_height = max(self.largest_target_height, self.timer_height + 1)
self.send_approval(self.timer_height + 1)
self.timer_started = now
self.timer_height += 1
def send_approval(self, target_height):
if target_height == self.head_height + 1:
inner = Endorsement(self.head_hash)
else:
inner = Skip(self.head_height)
approval = Approval(inner, target_height)
send(approval, to_whom = get_block_proposer(self.head_hash, target_height))
Where get_block_proposer
returns the next block proposer given the previous block and the height of the next block.
It is also necessary that ENDORSEMENT_DELAY < MIN_DELAY
. Moreover, while not necessary for correctness, we require that ENDORSEMENT_DELAY * 2 <= MIN_DELAY
.
Block Production
We first define a convenience function to fetch approvals that can be included in a block at particular height:
def get_approvals(self, target_height):
return [approval for approval
in self.approvals
if approval.target_height == target_height and
(isinstance(approval.inner, Skip) and approval.prev_height == self.head_height or
isinstance(approval.inner, Endorsement) and approval.prev_hash == self.head_hash)]
A block producer assigned for a particular height produces a block at that height whenever they have get_approvals
return approvals from block producers whose stake collectively exceeds 2/3 of the total stake.
Finality condition
A block is final in , where , when either or there is a block such that and . That is, either is the genesis block, or includes at least two blocks on top of , and these three blocks ( and the two following blocks) have consecutive heights.
Epoch switches
There's a parameter that defines the minimum length of an epoch. Suppose that a particular epoch started at height , and say the next epoch will be . Say is a set of block producers in epoch . Say is the highest final block in . The following are the rules of what blocks contain approvals from what block producers, and belong to what epoch.
- Any block with is in the epoch and must have approvals from more than of (stake-weighted).
- Any block with for which is in the epoch and must logically include approvals from both more than of and more than of (both stake-weighted).
- The first block with is in the epoch and must logically include approvals from more than of (stake-weighted).
(see the definition of logically including approvals in approval requirements)
Safety
Note that with the implementation above a honest block producer can never produce two endorsements with the same prev_height
(call this condition conflicting endorsements), neither can they produce a skip message s
and an endorsement e
such that s.prev_height < e.prev_height and s.target_height >= e.target_height
(call this condition conflicting skip and endorsement).
Theorem Suppose that there are blocks , , and such that , and . Then, more than of the block producer in some epoch must have signed either conflicting endorsements or conflicting skip and endorsement.
Proof Without loss of generality, we can assume that these blocks are chosen such that their heights are smallest possible. Specifically, we can assume that and . Also, letting be the highest block that is an ancestor of both and , we can assume that there is no block such that and or and .
Lemma There is such an epoch that all blocks such that or include approvals from more than of the block producers in .
Proof There are two cases.
Case 1: Blocks , and are all in the same epoch. Because the set of blocks in a given epoch in a given chain is a contiguous range, all blocks between them (specifically, all blocks such that or ) are also in the same epoch, so all those blocks include approvals from more than of the block producers in that epoch.
Case 2: Blocks , and are not all in the same epoch. Suppose that and are in different epochs. Let be the epoch of and be the preceding epoch ( cannot be in the same epoch as the genesis block). Let and be the first and the last block of in . Then, there must exist a block in epoch such that . Because , we have , and since there are no final blocks such that , we conclude that . Because there are no epochs between and , we conclude that is in epoch . Also, . Thus, any block after and until the end of must include approvals from more than of the block producers in . Applying the same argument to , we can determine that is either in or , and in both cases all blocks such that include approvals from more than of block producers in (the set of block producers in is the same in and because the last block of the epoch preceding , if any, is before and thus is shared by both chains). The case where and are in the same epoch, but and are in different epochs is handled similarly. Thus, the lemma is proven.
Now back to the theorem. Without loss of generality, assume that . On the one hand, if doesn't include a block at height , then the first block at height greater than must include skips from more than of the block producers in which conflict with endorsements in , therefore, more than of the block producers in must have signed conflicting skip and endorsement. Similarly, if doesn't include a block at height , more than of the block producers in signed both an endorsement in and a skip in the first block in at height greater than . On the other hand, if includes both a block at height and a block at height , the latter must include endorsements for the former, which conflict with endorsements for . Therefore, more than of the block producers in must have signed conflicting endorsements. Thus, the theorem is proven.
Liveness
See the proof of liveness in Doomslug Whitepaper and the recent Nightshade sharding protocol.
The consensus in this section differs in that it requires two consecutive blocks with endorsements. The proof in the linked paper trivially extends, by observing that once the delay is sufficiently long for a honest block producer to collect enough endorsements, the next block producer ought to have enough time to collect all the endorsements too.
Approval condition
The approval condition above
Any valid block must logically include approvals from block producers whose cumulative stake exceeds 2/3 of the total stake in the epoch. For a block
B
and its previous blockB'
each approval inB
must be anEndorsement
with the hash ofB'
if and only ifB.height == B'.height + 1
, otherwise it must be aSkip
with the height ofB'
Is more complex that desired, and it is tempting to unify the two conditions. Unfortunately, they cannot be unified.
It is critical that for endorsements each approval has the prev_hash
equal to the hash of the previous block, because otherwise the safety proof above doesn't work, in the second case the endorsements in B1
and Bx
can be the very same approvals.
It is critical that for the skip messages we do not require the hashes in the approvals to match the hash of the previous block, because otherwise a malicious actor can create two blocks at the same height, and distribute them such that half of the block producers have one as their head, and the other half has the other. The two halves of the block producers will be sending skip messages with different prev_hash
but the same prev_height
to the future block producers, and if there's a requirement that the prev_hash
in the skip matches exactly the prev_hash
of the block, no block producer will be able to create their blocks.