github.com/koko1123/flow-go-1@v0.29.6/engine/collection/compliance/core.go (about) 1 // (c) 2019 Dapper Labs - ALL RIGHTS RESERVED 2 3 package compliance 4 5 import ( 6 "errors" 7 "fmt" 8 9 "github.com/rs/zerolog" 10 11 "github.com/koko1123/flow-go-1/consensus/hotstuff" 12 "github.com/koko1123/flow-go-1/consensus/hotstuff/model" 13 "github.com/koko1123/flow-go-1/engine" 14 "github.com/koko1123/flow-go-1/model/cluster" 15 "github.com/koko1123/flow-go-1/model/flow" 16 "github.com/koko1123/flow-go-1/model/messages" 17 "github.com/koko1123/flow-go-1/module" 18 "github.com/koko1123/flow-go-1/module/compliance" 19 "github.com/koko1123/flow-go-1/module/metrics" 20 "github.com/koko1123/flow-go-1/state" 21 clusterkv "github.com/koko1123/flow-go-1/state/cluster" 22 "github.com/koko1123/flow-go-1/storage" 23 "github.com/koko1123/flow-go-1/utils/logging" 24 ) 25 26 // Core contains the central business logic for the collector clusters' compliance engine. 27 // It is responsible for handling communication for the embedded consensus algorithm. 28 // NOTE: Core is designed to be non-thread safe and cannot be used in concurrent environment 29 // user of this object needs to ensure single thread access. 30 type Core struct { 31 log zerolog.Logger // used to log relevant actions with context 32 config compliance.Config 33 metrics module.EngineMetrics 34 mempoolMetrics module.MempoolMetrics 35 collectionMetrics module.CollectionMetrics 36 headers storage.Headers 37 state clusterkv.MutableState 38 pending module.PendingClusterBlockBuffer // pending block cache 39 sync module.BlockRequester 40 hotstuff module.HotStuff 41 voteAggregator hotstuff.VoteAggregator 42 } 43 44 // NewCore instantiates the business logic for the collector clusters' compliance engine. 45 func NewCore( 46 log zerolog.Logger, 47 collector module.EngineMetrics, 48 mempool module.MempoolMetrics, 49 collectionMetrics module.CollectionMetrics, 50 headers storage.Headers, 51 state clusterkv.MutableState, 52 pending module.PendingClusterBlockBuffer, 53 voteAggregator hotstuff.VoteAggregator, 54 opts ...compliance.Opt, 55 ) (*Core, error) { 56 57 config := compliance.DefaultConfig() 58 for _, apply := range opts { 59 apply(&config) 60 } 61 62 c := &Core{ 63 log: log.With().Str("cluster_compliance", "core").Logger(), 64 config: config, 65 metrics: collector, 66 mempoolMetrics: mempool, 67 collectionMetrics: collectionMetrics, 68 headers: headers, 69 state: state, 70 pending: pending, 71 sync: nil, // use `WithSync` 72 hotstuff: nil, // use `WithConsensus` 73 voteAggregator: voteAggregator, 74 } 75 76 // log the mempool size off the bat 77 c.mempoolMetrics.MempoolEntries(metrics.ResourceClusterProposal, c.pending.Size()) 78 79 return c, nil 80 } 81 82 // OnBlockProposal handles incoming block proposals. 83 func (c *Core) OnBlockProposal(originID flow.Identifier, proposal *messages.ClusterBlockProposal) error { 84 block := proposal.Block.ToInternal() 85 header := block.Header 86 87 log := c.log.With(). 88 Hex("origin_id", originID[:]). 89 Str("chain_id", header.ChainID.String()). 90 Uint64("block_height", header.Height). 91 Uint64("block_view", header.View). 92 Hex("block_id", logging.Entity(header)). 93 Hex("parent_id", header.ParentID[:]). 94 Hex("ref_block_id", block.Payload.ReferenceBlockID[:]). 95 Hex("collection_id", logging.Entity(block.Payload.Collection)). 96 Int("tx_count", block.Payload.Collection.Len()). 97 Time("timestamp", header.Timestamp). 98 Hex("proposer", header.ProposerID[:]). 99 Hex("signers", header.ParentVoterIndices). 100 Logger() 101 log.Info().Msg("block proposal received") 102 103 // first, we reject all blocks that we don't need to process: 104 // 1) blocks already in the cache; they will already be processed later 105 // 2) blocks already on disk; they were processed and await finalization 106 107 // ignore proposals that are already cached 108 _, cached := c.pending.ByID(header.ID()) 109 if cached { 110 log.Debug().Msg("skipping already cached proposal") 111 return nil 112 } 113 114 // ignore proposals that were already processed 115 _, err := c.headers.ByBlockID(header.ID()) 116 if err == nil { 117 log.Debug().Msg("skipping already processed proposal") 118 return nil 119 } 120 if !errors.Is(err, storage.ErrNotFound) { 121 return fmt.Errorf("could not check proposal: %w", err) 122 } 123 124 // ignore proposals which are too far ahead of our local finalized state 125 // instead, rely on sync engine to catch up finalization more effectively, and avoid 126 // large subtree of blocks to be cached. 127 final, err := c.state.Final().Head() 128 if err != nil { 129 return fmt.Errorf("could not get latest finalized header: %w", err) 130 } 131 if header.Height > final.Height && header.Height-final.Height > c.config.SkipNewProposalsThreshold { 132 log.Debug(). 133 Uint64("final_height", final.Height). 134 Msg("dropping block too far ahead of locally finalized height") 135 return nil 136 } 137 138 // there are two possibilities if the proposal is neither already pending 139 // processing in the cache, nor has already been processed: 140 // 1) the proposal is unverifiable because the parent is unknown 141 // => we cache the proposal 142 // 2) the proposal is connected to finalized state through an unbroken chain 143 // => we verify the proposal and forward it to hotstuff if valid 144 145 // if the parent is a pending block (disconnected from the incorporated state), we cache this block as well. 146 // we don't have to request its parent block or its ancestor again, because as a 147 // pending block, its parent block must have been requested. 148 // if there was problem requesting its parent or ancestors, the sync engine's forward 149 // syncing with range requests for finalized blocks will request for the blocks. 150 _, found := c.pending.ByID(header.ParentID) 151 if found { 152 153 // add the block to the cache 154 _ = c.pending.Add(originID, block) 155 c.mempoolMetrics.MempoolEntries(metrics.ResourceClusterProposal, c.pending.Size()) 156 157 return nil 158 } 159 160 // if the proposal is connected to a block that is neither in the cache, nor 161 // in persistent storage, its direct parent is missing; cache the proposal 162 // and request the parent 163 _, err = c.headers.ByBlockID(header.ParentID) 164 if errors.Is(err, storage.ErrNotFound) { 165 166 _ = c.pending.Add(originID, block) 167 168 c.mempoolMetrics.MempoolEntries(metrics.ResourceClusterProposal, c.pending.Size()) 169 170 log.Debug().Msg("requesting missing parent for proposal") 171 172 c.sync.RequestBlock(header.ParentID, header.Height-1) 173 174 return nil 175 } 176 if err != nil { 177 return fmt.Errorf("could not check parent: %w", err) 178 } 179 180 // At this point, we should be able to connect the proposal to the finalized 181 // state and should process it to see whether to forward to hotstuff or not. 182 // processBlockAndDescendants is a recursive function. Here we trace the 183 // execution of the entire recursion, which might include processing the 184 // proposal's pending children. There is another span within 185 // processBlockProposal that measures the time spent for a single proposal. 186 err = c.processBlockAndDescendants(block) 187 c.mempoolMetrics.MempoolEntries(metrics.ResourceClusterProposal, c.pending.Size()) 188 if err != nil { 189 return fmt.Errorf("could not process block proposal: %w", err) 190 } 191 192 return nil 193 } 194 195 // processBlockAndDescendants is a recursive function that processes a block and 196 // its pending proposals for its children. By induction, any children connected 197 // to a valid proposal are validly connected to the finalized state and can be 198 // processed as well. 199 func (c *Core) processBlockAndDescendants(block *cluster.Block) error { 200 blockID := block.ID() 201 202 // process block itself 203 err := c.processBlockProposal(block) 204 // child is outdated by the time we started processing it 205 // => node was probably behind and is catching up. Log as warning 206 if engine.IsOutdatedInputError(err) { 207 c.log.Info().Msg("dropped processing of abandoned fork; this might be an indicator that the node is slightly behind") 208 return nil 209 } 210 // the block is invalid; log as error as we desire honest participation 211 // ToDo: potential slashing 212 if engine.IsInvalidInputError(err) { 213 c.log.Warn(). 214 Err(err). 215 Bool(logging.KeySuspicious, true). 216 Msg("received invalid block from other node (potential slashing evidence?)") 217 return nil 218 } 219 if engine.IsUnverifiableInputError(err) { 220 c.log.Warn(). 221 Err(err). 222 Msg("received unverifiable from other node") 223 return nil 224 } 225 if err != nil { 226 // unexpected error: potentially corrupted internal state => abort processing and escalate error 227 return fmt.Errorf("failed to process block %x: %w", blockID, err) 228 } 229 230 // process all children 231 // do not break on invalid or outdated blocks as they should not prevent us 232 // from processing other valid children 233 children, has := c.pending.ByParentID(blockID) 234 if !has { 235 return nil 236 } 237 for _, child := range children { 238 cpr := c.processBlockAndDescendants(child.Message) 239 if cpr != nil { 240 // unexpected error: potentially corrupted internal state => abort processing and escalate error 241 return cpr 242 } 243 } 244 245 // drop all the children that should have been processed now 246 c.pending.DropForParent(blockID) 247 248 return nil 249 } 250 251 // processBlockProposal processes the given block proposal. The proposal must connect to 252 // the finalized state. 253 func (c *Core) processBlockProposal(block *cluster.Block) error { 254 header := block.Header 255 log := c.log.With(). 256 Str("chain_id", header.ChainID.String()). 257 Uint64("block_height", header.Height). 258 Uint64("block_view", header.View). 259 Hex("block_id", logging.Entity(header)). 260 Hex("parent_id", header.ParentID[:]). 261 Hex("payload_hash", header.PayloadHash[:]). 262 Time("timestamp", header.Timestamp). 263 Hex("proposer", header.ProposerID[:]). 264 Hex("parent_signer_indices", header.ParentVoterIndices). 265 Logger() 266 log.Info().Msg("processing block proposal") 267 268 // see if the block is a valid extension of the protocol state 269 err := c.state.Extend(block) 270 // if the block proposes an invalid extension of the protocol state, then the block is invalid 271 if state.IsInvalidExtensionError(err) { 272 return engine.NewInvalidInputErrorf("invalid extension of cluster state (block_id: %x, height: %d): %w", 273 header.ID(), header.Height, err) 274 } 275 // protocol state aborted processing of block as it is on an abandoned fork: block is outdated 276 if state.IsOutdatedExtensionError(err) { 277 return engine.NewOutdatedInputErrorf("outdated extension of cluster state (block_id: %x, height: %d): %w", 278 header.ID(), header.Height, err) 279 } 280 if state.IsUnverifiableExtensionError(err) { 281 return engine.NewUnverifiableInputError("unverifiable extension of cluster state (block_id: %x, height: %d): %w", 282 header.ID(), header.Height, err) 283 } 284 if err != nil { 285 return fmt.Errorf("unexpected error while updating cluster state (block_id: %x, height: %d): %w", header.ID(), header.Height, err) 286 } 287 288 // retrieve the parent 289 parent, err := c.headers.ByBlockID(header.ParentID) 290 if err != nil { 291 return fmt.Errorf("could not retrieve proposal parent: %w", err) 292 } 293 294 // submit the model to hotstuff for processing 295 log.Info().Msg("forwarding block proposal to hotstuff") 296 // TODO: wait for the returned callback channel if we are processing blocks from range response 297 c.hotstuff.SubmitProposal(header, parent.View) 298 299 return nil 300 } 301 302 // OnBlockVote handles votes for blocks by passing them to the core consensus 303 // algorithm 304 func (c *Core) OnBlockVote(originID flow.Identifier, vote *messages.ClusterBlockVote) error { 305 306 c.log.Debug(). 307 Hex("origin_id", originID[:]). 308 Hex("block_id", vote.BlockID[:]). 309 Uint64("view", vote.View). 310 Msg("received vote") 311 312 c.voteAggregator.AddVote(&model.Vote{ 313 View: vote.View, 314 BlockID: vote.BlockID, 315 SignerID: originID, 316 SigData: vote.SigData, 317 }) 318 return nil 319 } 320 321 // ProcessFinalizedView performs pruning of stale data based on finalization event 322 // removes pending blocks below the finalized view 323 func (c *Core) ProcessFinalizedView(finalizedView uint64) { 324 // remove all pending blocks at or below the finalized view 325 c.pending.PruneByView(finalizedView) 326 327 // always record the metric 328 c.mempoolMetrics.MempoolEntries(metrics.ResourceClusterProposal, c.pending.Size()) 329 }