github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/engine/common/follower/pending_tree/pending_tree.go (about) 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 // PendingBlockVertex wraps a block proposal to implement forest.Vertex 13 // so the proposal can be stored in forest.LevelledForest 14 type PendingBlockVertex struct { 15 flow.CertifiedBlock 16 connectedToFinalized bool 17 } 18 19 var _ forest.Vertex = (*PendingBlockVertex)(nil) 20 21 // NewVertex creates new vertex while performing a sanity check of data correctness. 22 func NewVertex(certifiedBlock flow.CertifiedBlock, connectedToFinalized bool) (*PendingBlockVertex, error) { 23 return &PendingBlockVertex{ 24 CertifiedBlock: certifiedBlock, 25 connectedToFinalized: connectedToFinalized, 26 }, nil 27 } 28 29 func (v *PendingBlockVertex) VertexID() flow.Identifier { return v.CertifyingQC.BlockID } 30 func (v *PendingBlockVertex) Level() uint64 { return v.CertifyingQC.View } 31 func (v *PendingBlockVertex) Parent() (flow.Identifier, uint64) { 32 return v.Block.Header.ParentID, v.Block.Header.ParentView 33 } 34 35 // PendingTree is a mempool holding certified blocks that eventually might be connected to the finalized state. 36 // As soon as a valid fork of certified blocks descending from the latest finalized block is observed, 37 // we pass this information to caller. Internally, the mempool utilizes the LevelledForest. 38 // PendingTree is NOT safe to use in concurrent environment. 39 // Note: 40 // - The ability to skip ahead is irrelevant for staked nodes, which continuously follow the chain. 41 // However, light clients don't necessarily follow the chain block by block. Assume a light client 42 // that knows the EpochCommit event, i.e. the consensus committee authorized to certify blocks. A 43 // staked node can easily ship a proof of finalization for a block within that epoch to such a 44 // light client. This would be much cheaper for the light client than downloading the headers for 45 // all blocks in the epoch. 46 // - The pending tree supports skipping ahead, as this is a more general and simpler algorithm. 47 // Removing the ability to skip ahead would restrict the PendingTree's domain of potential 48 // applications _and_ would require additional code and additional tests making it more complex. 49 // 50 // Outlook: 51 // - At the moment, PendingTree relies on notion of a `Certified Block` which is a valid block accompanied 52 // by a certifying QC (proving block validity). This works well for consensus follower, as it is designed 53 // to work with certified blocks. 54 // - In the future, we could use the PendingTree also for consensus participants. Therefore, we would need 55 // to abstract out CertifiedBlock or replace it with a generic argument that satisfies some contract 56 // (returns View, Height, BlockID). Then, consensus participants could use the Pending Tree without 57 // QCs and instead fully validate inbound blocks (incl. payloads) to guarantee block validity. 58 type PendingTree struct { 59 forest *forest.LevelledForest 60 lastFinalizedID flow.Identifier 61 } 62 63 // NewPendingTree creates new instance of PendingTree. Accepts finalized block to set up initial state. 64 func NewPendingTree(finalized *flow.Header) *PendingTree { 65 return &PendingTree{ 66 forest: forest.NewLevelledForest(finalized.View), 67 lastFinalizedID: finalized.ID(), 68 } 69 } 70 71 // AddBlocks accepts a batch of certified blocks, adds them to the tree of pending blocks and finds blocks connected to 72 // the finalized state. 73 // 74 // Details: 75 // Adding blocks might result in additional blocks now being connected to the latest finalized block. The returned 76 // slice contains: 77 // 1. the subset of `certifiedBlocks` that are connected to the finalized block 78 // - excluding any blocks whose view is smaller or equal to the finalized block 79 // - if a block `B ∈ certifiedBlocks` is already known to the PendingTree and connected, 80 // `B` and all its connected descendants will be in the returned list 81 // 2. additionally, all of the _connected_ descendants of the blocks from step 1. 82 // 83 // PendingTree treats its input as a potentially repetitive stream of information: repeated inputs are already 84 // consistent with the current state. While repetitive inputs might cause repetitive outputs, the implementation 85 // has some general heuristics to avoid extra work: 86 // - It drops blocks whose view is smaller or equal to the finalized block 87 // - It deduplicates incoming blocks. We don't store additional vertices in tree if we have that block already stored. 88 // 89 // Expected errors during normal operations: 90 // - model.ByzantineThresholdExceededError - detected two certified blocks at the same view 91 // 92 // All other errors should be treated as exceptions. 93 func (t *PendingTree) AddBlocks(certifiedBlocks []flow.CertifiedBlock) ([]flow.CertifiedBlock, error) { 94 var allConnectedBlocks []flow.CertifiedBlock 95 for _, block := range certifiedBlocks { 96 // skip blocks lower than finalized view 97 if block.View() <= t.forest.LowestLevel { 98 continue 99 } 100 101 iter := t.forest.GetVerticesAtLevel(block.View()) 102 if iter.HasNext() { 103 v := iter.NextVertex().(*PendingBlockVertex) 104 105 if v.VertexID() == block.ID() { 106 // this vertex is already in tree, skip it 107 continue 108 } else { 109 return nil, model.ByzantineThresholdExceededError{Evidence: fmt.Sprintf( 110 "conflicting QCs at view %d: %v and %v", 111 block.View(), v.ID(), block.ID(), 112 )} 113 } 114 } 115 116 vertex, err := NewVertex(block, false) 117 if err != nil { 118 return nil, fmt.Errorf("could not create new vertex: %w", err) 119 } 120 err = t.forest.VerifyVertex(vertex) 121 if err != nil { 122 return nil, fmt.Errorf("failed to store certified block into the tree: %w", err) 123 } 124 t.forest.AddVertex(vertex) 125 126 if t.connectsToFinalizedBlock(block) { 127 allConnectedBlocks = t.updateAndCollectFork(allConnectedBlocks, vertex) 128 } 129 } 130 131 return allConnectedBlocks, nil 132 } 133 134 // connectsToFinalizedBlock checks if candidate block connects to the finalized state. 135 func (t *PendingTree) connectsToFinalizedBlock(block flow.CertifiedBlock) bool { 136 if block.Block.Header.ParentID == t.lastFinalizedID { 137 return true 138 } 139 if parentVertex, found := t.forest.GetVertex(block.Block.Header.ParentID); found { 140 return parentVertex.(*PendingBlockVertex).connectedToFinalized 141 } 142 return false 143 } 144 145 // FinalizeFork takes last finalized block and prunes all blocks below the finalized view. 146 // PendingTree treats its input as a potentially repetitive stream of information: repeated 147 // and older inputs (out of order) are already consistent with the current state. Repetitive 148 // inputs might cause repetitive outputs. 149 // When a block is finalized we don't care for any blocks below it, since they were already finalized. 150 // Finalizing a block might causes the pending PendingTree to detect _additional_ blocks as now 151 // being connected to the latest finalized block. This happens if some connecting blocks are missing 152 // and then a block higher than the missing blocks is finalized. 153 // In the following example, B is the last finalized block known to the PendingTree 154 // 155 // A ← B ←-?-?-?-- X ← Y ← Z 156 // 157 // The network has already progressed to finalizing block X. However, the interim blocks denoted 158 // by '←-?-?-?--' have not been received by our PendingTree. Therefore, we still consider X,Y,Z 159 // as disconnected. If the PendingTree tree is now informed that X is finalized, it can fast- 160 // forward to the respective state, as it anyway would prune all the blocks below X. 161 // Note: 162 // - The ability to skip ahead is irrelevant for staked nodes, which continuously follows the chain. 163 // However, light clients don't necessarily follow the chain block by block. Assume a light client 164 // that knows the EpochCommit event, i.e. the consensus committee authorized to certify blocks. A 165 // staked node can easily ship a proof of finalization for a block within that epoch to such a 166 // light client. This would be much cheaper for the light client than downloading the headers for 167 // all blocks in the epoch. 168 // - The pending tree supports skipping ahead, as this is a more general and simpler algorithm. 169 // Removing the ability to skip ahead would restrict the PendingTree's its domain of potential 170 // applications _and_ would require additional code and additional tests making it more complex. 171 // 172 // If the PendingTree detects additional blocks as descending from the latest finalized block, it 173 // returns these blocks. Returned blocks are ordered such that parents appear before their children. 174 // 175 // No errors are expected during normal operation. 176 func (t *PendingTree) FinalizeFork(finalized *flow.Header) ([]flow.CertifiedBlock, error) { 177 var connectedBlocks []flow.CertifiedBlock 178 179 err := t.forest.PruneUpToLevel(finalized.View) 180 if err != nil { 181 if mempool.IsBelowPrunedThresholdError(err) { 182 return nil, nil 183 } 184 return connectedBlocks, fmt.Errorf("could not prune tree up to view %d: %w", finalized.View, err) 185 } 186 t.lastFinalizedID = finalized.ID() 187 188 iter := t.forest.GetChildren(t.lastFinalizedID) 189 for iter.HasNext() { 190 v := iter.NextVertex().(*PendingBlockVertex) 191 connectedBlocks = t.updateAndCollectFork(connectedBlocks, v) 192 } 193 194 return connectedBlocks, nil 195 } 196 197 // updateAndCollectFork marks the subtree rooted at `vertex.Block` as connected to the finalized state 198 // and returns all blocks in this subtree. No parents of `vertex.Block` are modified or included in the output. 199 // The output list will be ordered so that parents appear before children. 200 // The caller must ensure that `vertex.Block` is connected to the finalized state. 201 // 202 // A ← B ← C ←D 203 // ↖ E 204 // 205 // For example, suppose B is the input vertex. Then: 206 // - A must already be connected to the finalized state 207 // - B, E, C, D are marked as connected to the finalized state and included in the output list 208 // 209 // This method has a similar signature as `append` for performance reasons: 210 // - any connected certified blocks are appended to `queue` 211 // - we return the _resulting slice_ after all appends 212 func (t *PendingTree) updateAndCollectFork(queue []flow.CertifiedBlock, vertex *PendingBlockVertex) []flow.CertifiedBlock { 213 if vertex.connectedToFinalized { 214 return queue // no-op if already connected 215 } 216 vertex.connectedToFinalized = true 217 queue = append(queue, vertex.CertifiedBlock) 218 219 iter := t.forest.GetChildren(vertex.VertexID()) 220 for iter.HasNext() { 221 nextVertex := iter.NextVertex().(*PendingBlockVertex) 222 queue = t.updateAndCollectFork(queue, nextVertex) 223 } 224 return queue 225 }