Skip to content

Commit f4dcc4d

Browse files
Merge #4003
4003: [Follower] Implementation of `PendingTree` r=durkmurder a=durkmurder https://github.com/dapperlabs/flow-go/issues/6491 ### Context This PR implements a `PendingTree`, a dedicated structure for storing blocks that are certified but not yet connected to the finalized state. `PendingTree` was designed with a few assumptions and it's reflected in the implementation: - Blocks are delivered in batches. - Blocks can be delivered in random order inside of batch. - Blocks can be delivered with skips in height/view. - Blocks HAS to be delivered by same goroutine in. Implementation is not concurrently safe. - `PendingTree` uses views for levels to detect reaching byzantine threshold. - `PendingTree` doesn't deal with QC in any way, everything is abstracted out behind `CertifiedBlock` to keep the implementation flexible enough to support usage in consensus participant(using generics). - Blocks stored in tree are resolved when processing incoming blocks. Co-authored-by: Yurii Oleksyshyn <yuraolex@gmail.com>
2 parents 0375df1 + aa99bea commit f4dcc4d

File tree

3 files changed

+509
-3
lines changed

3 files changed

+509
-3
lines changed
Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
package pending_tree
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/onflow/flow-go/consensus/hotstuff/model"
7+
"github.com/onflow/flow-go/model/flow"
8+
"github.com/onflow/flow-go/module/forest"
9+
"github.com/onflow/flow-go/module/mempool"
10+
)
11+
12+
// CertifiedBlock holds a certified block, it consists of a block and a QC which proves validity of block (QC.BlockID = Block.ID())
13+
// This is used to compactly store and transport block and certifying QC in one structure.
14+
type CertifiedBlock struct {
15+
Block *flow.Block
16+
QC *flow.QuorumCertificate
17+
}
18+
19+
// ID returns unique identifier for the certified block
20+
// To avoid computation we use value from the QC
21+
func (b *CertifiedBlock) ID() flow.Identifier {
22+
return b.QC.BlockID
23+
}
24+
25+
// View returns view where the block was produced.
26+
func (b *CertifiedBlock) View() uint64 {
27+
return b.QC.View
28+
}
29+
30+
// Height returns height of the block.
31+
func (b *CertifiedBlock) Height() uint64 {
32+
return b.Block.Header.Height
33+
}
34+
35+
// PendingBlockVertex wraps a block proposal to implement forest.Vertex
36+
// so the proposal can be stored in forest.LevelledForest
37+
type PendingBlockVertex struct {
38+
CertifiedBlock
39+
connectedToFinalized bool
40+
}
41+
42+
var _ forest.Vertex = (*PendingBlockVertex)(nil)
43+
44+
// NewVertex creates new vertex while performing a sanity check of data correctness.
45+
func NewVertex(certifiedBlock CertifiedBlock, connectedToFinalized bool) (*PendingBlockVertex, error) {
46+
if certifiedBlock.Block.Header.View != certifiedBlock.QC.View {
47+
return nil, fmt.Errorf("missmatched block(%d) and QC(%d) view",
48+
certifiedBlock.Block.Header.View, certifiedBlock.QC.View)
49+
}
50+
return &PendingBlockVertex{
51+
CertifiedBlock: certifiedBlock,
52+
connectedToFinalized: connectedToFinalized,
53+
}, nil
54+
}
55+
56+
func (v *PendingBlockVertex) VertexID() flow.Identifier { return v.QC.BlockID }
57+
func (v *PendingBlockVertex) Level() uint64 { return v.QC.View }
58+
func (v *PendingBlockVertex) Parent() (flow.Identifier, uint64) {
59+
return v.Block.Header.ParentID, v.Block.Header.ParentView
60+
}
61+
62+
// PendingTree is a mempool holding certified blocks that eventually might be connected to the finalized state.
63+
// As soon as a valid fork of certified blocks descending from the latest finalized block is observed,
64+
// we pass this information to caller. Internally, the mempool utilizes the LevelledForest.
65+
// PendingTree is NOT safe to use in concurrent environment.
66+
// NOTE: PendingTree relies on notion of `CertifiedBlock` which is a valid block accompanied by a certifying QC (proving block validity).
67+
// This works well for consensus follower as it is designed to work with certified blocks. To use this structure for consensus
68+
// participant we can abstract out CertifiedBlock or replace it with a generic argument that satisfies some contract(returns View, Height, BlockID).
69+
// With this change this structure can be used by consensus participant for tracking connection to the finalized state even without
70+
// having QC but relying on payload validation.
71+
type PendingTree struct {
72+
forest *forest.LevelledForest
73+
lastFinalizedID flow.Identifier
74+
}
75+
76+
// NewPendingTree creates new instance of PendingTree. Accepts finalized block to set up initial state.
77+
func NewPendingTree(finalized *flow.Header) *PendingTree {
78+
return &PendingTree{
79+
forest: forest.NewLevelledForest(finalized.View),
80+
lastFinalizedID: finalized.ID(),
81+
}
82+
}
83+
84+
// AddBlocks accepts a batch of certified blocks, adds them to the tree of pending blocks and finds blocks connected to the finalized state.
85+
// This function performs processing of incoming certified blocks, implementation is split into a few different sections
86+
// but tries to be optimal in terms of performance to avoid doing extra work as much as possible.
87+
// This function proceeds as follows:
88+
// 1. Sorts incoming batch by height. Since blocks can be submitted in random order we need to find blocks with
89+
// the lowest height since they are candidates for being connected to the finalized state.
90+
// 2. Filters out blocks that are already finalized.
91+
// 3. Deduplicates incoming blocks. We don't store additional vertices in tree if we have that block already stored.
92+
// 4. Checks for exceeding byzantine threshold. Only one certified block per view is allowed.
93+
// 5. Finally, blocks with the lowest height from incoming batch that connect to the finalized state we will
94+
// mark all descendants as connected, collect them and return as result of invocation.
95+
//
96+
// This function is designed to perform resolution of connected blocks(resolved block is the one that connects to the finalized state)
97+
// using incoming batch. Each block that was connected to the finalized state is reported once.
98+
// Expected errors during normal operations:
99+
// - model.ByzantineThresholdExceededError - detected two certified blocks at the same view
100+
//
101+
// All other errors should be treated as exceptions.
102+
func (t *PendingTree) AddBlocks(certifiedBlocks []CertifiedBlock) ([]CertifiedBlock, error) {
103+
var allConnectedBlocks []CertifiedBlock
104+
for _, block := range certifiedBlocks {
105+
// skip blocks lower than finalized view
106+
if block.View() <= t.forest.LowestLevel {
107+
continue
108+
}
109+
110+
iter := t.forest.GetVerticesAtLevel(block.View())
111+
if iter.HasNext() {
112+
v := iter.NextVertex().(*PendingBlockVertex)
113+
114+
if v.VertexID() == block.ID() {
115+
// this vertex is already in tree, skip it
116+
continue
117+
} else {
118+
return nil, model.ByzantineThresholdExceededError{Evidence: fmt.Sprintf(
119+
"conflicting QCs at view %d: %v and %v",
120+
block.View(), v.ID(), block.ID(),
121+
)}
122+
}
123+
}
124+
125+
vertex, err := NewVertex(block, false)
126+
if err != nil {
127+
return nil, fmt.Errorf("could not create new vertex: %w", err)
128+
}
129+
err = t.forest.VerifyVertex(vertex)
130+
if err != nil {
131+
return nil, fmt.Errorf("failed to store certified block into the tree: %w", err)
132+
}
133+
t.forest.AddVertex(vertex)
134+
135+
if t.connectsToFinalizedBlock(block) {
136+
allConnectedBlocks = t.updateAndCollectFork(allConnectedBlocks, vertex)
137+
}
138+
}
139+
140+
return allConnectedBlocks, nil
141+
}
142+
143+
// connectsToFinalizedBlock checks if candidate block connects to the finalized state.
144+
func (t *PendingTree) connectsToFinalizedBlock(block CertifiedBlock) bool {
145+
if block.Block.Header.ParentID == t.lastFinalizedID {
146+
return true
147+
}
148+
if parentVertex, found := t.forest.GetVertex(block.Block.Header.ParentID); found {
149+
return parentVertex.(*PendingBlockVertex).connectedToFinalized
150+
}
151+
return false
152+
}
153+
154+
// FinalizeFork takes last finalized block and prunes all blocks below the finalized view.
155+
// PendingTree treats its input as a potentially repetitive stream of information: repeated
156+
// and older inputs (out of order) are already consistent with the current state. Repetitive
157+
// inputs might cause repetitive outputs.
158+
// When a block is finalized we don't care for any blocks below it, since they were already finalized.
159+
// Finalizing a block might causes the pending PendingTree to detect _additional_ blocks as now
160+
// being connected to the latest finalized block. This happens of some connecting blocks are missing
161+
// and then a block higher than the missing blocks is finalized.
162+
// In the following example, B is the last finalized block known to the PendingTree
163+
//
164+
// A ← B ←-?-?-?-- X ← Y ← Z
165+
//
166+
// The network has already progressed to finalizing block X. However, the interim blocks denoted
167+
// by '←-?-?-?--' have not been received by our PendingTree. Therefore, we still consider X,Y,Z
168+
// as disconnected. If the PendingTree tree is now informed that X is finalized, it can fast-
169+
// forward to the respective state, as it anyway would prune all the blocks below X.
170+
//
171+
// If the PendingTree detect additional blocks as descending from the latest finalized block, it
172+
// returns these blocks. Returned blocks are ordered such that parents appear before their children.
173+
//
174+
// No errors are expected during normal operation.
175+
func (t *PendingTree) FinalizeFork(finalized *flow.Header) ([]CertifiedBlock, error) {
176+
var connectedBlocks []CertifiedBlock
177+
178+
err := t.forest.PruneUpToLevel(finalized.View)
179+
if err != nil {
180+
if mempool.IsBelowPrunedThresholdError(err) {
181+
return nil, nil
182+
}
183+
return connectedBlocks, fmt.Errorf("could not prune tree up to view %d: %w", finalized.View, err)
184+
}
185+
t.lastFinalizedID = finalized.ID()
186+
187+
iter := t.forest.GetChildren(t.lastFinalizedID)
188+
for iter.HasNext() {
189+
v := iter.NextVertex().(*PendingBlockVertex)
190+
connectedBlocks = t.updateAndCollectFork(connectedBlocks, v)
191+
}
192+
193+
return connectedBlocks, nil
194+
}
195+
196+
// updateAndCollectFork marks the subtree rooted at `vertex.Block` as connected to the finalized state
197+
// and returns all blocks in this subtree. No parents of `vertex.Block` are modified or included in the output.
198+
// The output list will be ordered so that parents appear before children.
199+
// The caller must ensure that `vertex.Block` is connected to the finalized state.
200+
//
201+
// A ← B ← C ←D
202+
// ↖ E
203+
//
204+
// For example, suppose B is the input vertex. Then:
205+
// - A must already be connected to the finalized state
206+
// - B, E, C, D are marked as connected to the finalized state and included in the output list
207+
//
208+
// This method has a similar signature as `append` for performance reasons:
209+
// - any connected certified blocks are appended to `queue`
210+
// - we return the _resulting slice_ after all appends
211+
func (t *PendingTree) updateAndCollectFork(queue []CertifiedBlock, vertex *PendingBlockVertex) []CertifiedBlock {
212+
if vertex.connectedToFinalized {
213+
return queue // no-op if already connected
214+
}
215+
vertex.connectedToFinalized = true
216+
queue = append(queue, vertex.CertifiedBlock)
217+
218+
iter := t.forest.GetChildren(vertex.VertexID())
219+
for iter.HasNext() {
220+
nextVertex := iter.NextVertex().(*PendingBlockVertex)
221+
queue = t.updateAndCollectFork(queue, nextVertex)
222+
}
223+
return queue
224+
}

0 commit comments

Comments
 (0)