github.com/onflow/flow-go@v0.33.17/engine/consensus/compliance/core.go (about) 1 // (c) 2019 Dapper Labs - ALL RIGHTS RESERVED 2 3 package compliance 4 5 import ( 6 "context" 7 "errors" 8 "fmt" 9 "time" 10 11 "github.com/rs/zerolog" 12 "go.opentelemetry.io/otel/attribute" 13 14 "github.com/onflow/flow-go/consensus/hotstuff" 15 "github.com/onflow/flow-go/consensus/hotstuff/model" 16 "github.com/onflow/flow-go/engine" 17 "github.com/onflow/flow-go/model/flow" 18 "github.com/onflow/flow-go/model/messages" 19 "github.com/onflow/flow-go/module" 20 "github.com/onflow/flow-go/module/compliance" 21 "github.com/onflow/flow-go/module/counters" 22 "github.com/onflow/flow-go/module/mempool" 23 "github.com/onflow/flow-go/module/metrics" 24 "github.com/onflow/flow-go/module/trace" 25 "github.com/onflow/flow-go/state" 26 "github.com/onflow/flow-go/state/protocol" 27 "github.com/onflow/flow-go/storage" 28 "github.com/onflow/flow-go/utils/logging" 29 ) 30 31 // Core contains the central business logic for the main consensus' compliance engine. 32 // It is responsible for handling communication for the embedded consensus algorithm. 33 // CAUTION with CONCURRENCY: 34 // - At the moment, compliance.Core _can not_ process blocks concurrently. Callers of `OnBlockProposal` 35 // need to ensure single-threaded access. 36 // - The only exception is calls to `ProcessFinalizedView`, which is the only concurrency-safe 37 // method of compliance.Core 38 type Core struct { 39 log zerolog.Logger // used to log relevant actions with context 40 config compliance.Config 41 engineMetrics module.EngineMetrics 42 mempoolMetrics module.MempoolMetrics 43 hotstuffMetrics module.HotstuffMetrics 44 complianceMetrics module.ComplianceMetrics 45 proposalViolationNotifier hotstuff.ProposalViolationConsumer 46 tracer module.Tracer 47 headers storage.Headers 48 payloads storage.Payloads 49 state protocol.ParticipantState 50 // track latest finalized view/height - used to efficiently drop outdated or too-far-ahead blocks 51 finalizedView counters.StrictMonotonousCounter 52 finalizedHeight counters.StrictMonotonousCounter 53 pending module.PendingBlockBuffer // pending block cache 54 sync module.BlockRequester 55 hotstuff module.HotStuff 56 validator hotstuff.Validator 57 voteAggregator hotstuff.VoteAggregator 58 timeoutAggregator hotstuff.TimeoutAggregator 59 } 60 61 // NewCore instantiates the business logic for the main consensus' compliance engine. 62 func NewCore( 63 log zerolog.Logger, 64 collector module.EngineMetrics, 65 mempool module.MempoolMetrics, 66 hotstuffMetrics module.HotstuffMetrics, 67 complianceMetrics module.ComplianceMetrics, 68 proposalViolationNotifier hotstuff.ProposalViolationConsumer, 69 tracer module.Tracer, 70 headers storage.Headers, 71 payloads storage.Payloads, 72 state protocol.ParticipantState, 73 pending module.PendingBlockBuffer, 74 sync module.BlockRequester, 75 validator hotstuff.Validator, 76 hotstuff module.HotStuff, 77 voteAggregator hotstuff.VoteAggregator, 78 timeoutAggregator hotstuff.TimeoutAggregator, 79 config compliance.Config, 80 ) (*Core, error) { 81 82 c := &Core{ 83 log: log.With().Str("compliance", "core").Logger(), 84 config: config, 85 engineMetrics: collector, 86 tracer: tracer, 87 mempoolMetrics: mempool, 88 hotstuffMetrics: hotstuffMetrics, 89 complianceMetrics: complianceMetrics, 90 proposalViolationNotifier: proposalViolationNotifier, 91 headers: headers, 92 payloads: payloads, 93 state: state, 94 pending: pending, 95 sync: sync, 96 hotstuff: hotstuff, 97 validator: validator, 98 voteAggregator: voteAggregator, 99 timeoutAggregator: timeoutAggregator, 100 } 101 102 // initialize finalized boundary cache 103 final, err := c.state.Final().Head() 104 if err != nil { 105 return nil, fmt.Errorf("could not initialized finalized boundary cache: %w", err) 106 } 107 c.ProcessFinalizedBlock(final) 108 109 c.mempoolMetrics.MempoolEntries(metrics.ResourceProposal, c.pending.Size()) 110 111 return c, nil 112 } 113 114 // OnBlockProposal handles incoming block proposals. 115 // No errors are expected during normal operation. All returned exceptions 116 // are potential symptoms of internal state corruption and should be fatal. 117 func (c *Core) OnBlockProposal(proposal flow.Slashable[*messages.BlockProposal]) error { 118 block := flow.Slashable[*flow.Block]{ 119 OriginID: proposal.OriginID, 120 Message: proposal.Message.Block.ToInternal(), 121 } 122 header := block.Message.Header 123 blockID := header.ID() 124 finalHeight := c.finalizedHeight.Value() 125 finalView := c.finalizedView.Value() 126 127 span, _ := c.tracer.StartBlockSpan(context.Background(), header.ID(), trace.CONCompOnBlockProposal) 128 span.SetAttributes( 129 attribute.Int64("view", int64(header.View)), 130 attribute.String("origin_id", proposal.OriginID.String()), 131 attribute.String("proposer", header.ProposerID.String()), 132 ) 133 traceID := span.SpanContext().TraceID().String() 134 defer span.End() 135 136 log := c.log.With(). 137 Hex("origin_id", proposal.OriginID[:]). 138 Str("chain_id", header.ChainID.String()). 139 Uint64("block_height", header.Height). 140 Uint64("block_view", header.View). 141 Hex("block_id", logging.Entity(header)). 142 Hex("parent_id", header.ParentID[:]). 143 Hex("payload_hash", header.PayloadHash[:]). 144 Time("timestamp", header.Timestamp). 145 Hex("proposer", header.ProposerID[:]). 146 Hex("parent_signer_indices", header.ParentVoterIndices). 147 Str("traceID", traceID). // traceID is used to connect logs to traces 148 Uint64("finalized_height", finalHeight). 149 Uint64("finalized_view", finalView). 150 Logger() 151 log.Info().Msg("block proposal received") 152 153 // drop proposals below the finalized threshold 154 if header.Height <= finalHeight || header.View <= finalView { 155 log.Debug().Msg("dropping block below finalized boundary") 156 return nil 157 } 158 159 skipNewProposalsThreshold := c.config.GetSkipNewProposalsThreshold() 160 // ignore proposals which are too far ahead of our local finalized state 161 // instead, rely on sync engine to catch up finalization more effectively, and avoid 162 // large subtree of blocks to be cached. 163 if header.View > finalView+skipNewProposalsThreshold { 164 log.Debug(). 165 Uint64("skip_new_proposals_threshold", skipNewProposalsThreshold). 166 Msg("dropping block too far ahead of locally finalized view") 167 return nil 168 } 169 170 // first, we reject all blocks that we don't need to process: 171 // 1) blocks already in the cache; they will already be processed later 172 // 2) blocks already on disk; they were processed and await finalization 173 174 // ignore proposals that are already cached 175 _, cached := c.pending.ByID(blockID) 176 if cached { 177 log.Debug().Msg("skipping already cached proposal") 178 return nil 179 } 180 181 // ignore proposals that were already processed 182 _, err := c.headers.ByBlockID(blockID) 183 if err == nil { 184 log.Debug().Msg("skipping already processed proposal") 185 return nil 186 } 187 if !errors.Is(err, storage.ErrNotFound) { 188 return fmt.Errorf("could not check proposal: %w", err) 189 } 190 191 // there are two possibilities if the proposal is neither already pending 192 // processing in the cache, nor has already been processed: 193 // 1) the proposal is unverifiable because the parent is unknown 194 // => we cache the proposal 195 // 2) the proposal is connected to finalized state through an unbroken chain 196 // => we verify the proposal and forward it to hotstuff if valid 197 198 // if the parent is a pending block (disconnected from the incorporated state), we cache this block as well. 199 // we don't have to request its parent block or its ancestor again, because as a 200 // pending block, its parent block must have been requested. 201 // if there was problem requesting its parent or ancestors, the sync engine's forward 202 // syncing with range requests for finalized blocks will request for the blocks. 203 _, found := c.pending.ByID(header.ParentID) 204 if found { 205 // add the block to the cache 206 _ = c.pending.Add(block) 207 c.mempoolMetrics.MempoolEntries(metrics.ResourceProposal, c.pending.Size()) 208 209 return nil 210 } 211 212 // if the proposal is connected to a block that is neither in the cache, nor 213 // in persistent storage, its direct parent is missing; cache the proposal 214 // and request the parent 215 exists, err := c.headers.Exists(header.ParentID) 216 if err != nil { 217 return fmt.Errorf("could not check parent exists: %w", err) 218 } 219 if !exists { 220 _ = c.pending.Add(block) 221 c.mempoolMetrics.MempoolEntries(metrics.ResourceProposal, c.pending.Size()) 222 223 c.sync.RequestBlock(header.ParentID, header.Height-1) 224 log.Debug().Msg("requesting missing parent for proposal") 225 return nil 226 } 227 228 // At this point, we should be able to connect the proposal to the finalized 229 // state and should process it to see whether to forward to hotstuff or not. 230 // processBlockAndDescendants is a recursive function. Here we trace the 231 // execution of the entire recursion, which might include processing the 232 // proposal's pending children. There is another span within 233 // processBlockProposal that measures the time spent for a single proposal. 234 err = c.processBlockAndDescendants(block) 235 c.mempoolMetrics.MempoolEntries(metrics.ResourceProposal, c.pending.Size()) 236 if err != nil { 237 return fmt.Errorf("could not process block proposal: %w", err) 238 } 239 240 return nil 241 } 242 243 // processBlockAndDescendants is a recursive function that processes a block and 244 // its pending proposals for its children. By induction, any children connected 245 // to a valid proposal are validly connected to the finalized state and can be 246 // processed as well. 247 // No errors are expected during normal operation. All returned exceptions 248 // are potential symptoms of internal state corruption and should be fatal. 249 func (c *Core) processBlockAndDescendants(proposal flow.Slashable[*flow.Block]) error { 250 header := proposal.Message.Header 251 blockID := header.ID() 252 253 log := c.log.With(). 254 Str("block_id", blockID.String()). 255 Uint64("block_height", header.Height). 256 Uint64("block_view", header.View). 257 Uint64("parent_view", header.ParentView). 258 Logger() 259 260 // process block itself 261 err := c.processBlockProposal(proposal.Message) 262 if err != nil { 263 if checkForAndLogOutdatedInputError(err, log) || checkForAndLogUnverifiableInputError(err, log) { 264 return nil 265 } 266 if invalidBlockErr, ok := model.AsInvalidProposalError(err); ok { 267 log.Err(err).Msg("received invalid block from other node (potential slashing evidence?)") 268 269 // notify consumers about invalid block 270 c.proposalViolationNotifier.OnInvalidBlockDetected(flow.Slashable[model.InvalidProposalError]{ 271 OriginID: proposal.OriginID, 272 Message: *invalidBlockErr, 273 }) 274 275 // notify VoteAggregator about the invalid block 276 err = c.voteAggregator.InvalidBlock(model.ProposalFromFlow(header)) 277 if err != nil { 278 if mempool.IsBelowPrunedThresholdError(err) { 279 log.Warn().Msg("received invalid block, but is below pruned threshold") 280 return nil 281 } 282 return fmt.Errorf("unexpected error notifying vote aggregator about invalid block: %w", err) 283 } 284 return nil 285 } 286 // unexpected error: potentially corrupted internal state => abort processing and escalate error 287 return fmt.Errorf("failed to process block %x: %w", blockID, err) 288 } 289 290 // process all children 291 // do not break on invalid or outdated blocks as they should not prevent us 292 // from processing other valid children 293 children, has := c.pending.ByParentID(blockID) 294 if !has { 295 return nil 296 } 297 for _, child := range children { 298 cpr := c.processBlockAndDescendants(child) 299 if cpr != nil { 300 // unexpected error: potentially corrupted internal state => abort processing and escalate error 301 return cpr 302 } 303 } 304 305 // drop all the children that should have been processed now 306 c.pending.DropForParent(blockID) 307 308 return nil 309 } 310 311 // processBlockProposal processes the given block proposal. The proposal must connect to 312 // the finalized state. 313 // Expected errors during normal operations: 314 // - engine.OutdatedInputError if the block proposal is outdated (e.g. orphaned) 315 // - model.InvalidProposalError if the block proposal is invalid 316 // - engine.UnverifiableInputError if the block proposal cannot be verified 317 func (c *Core) processBlockProposal(proposal *flow.Block) error { 318 startTime := time.Now() 319 defer func() { 320 c.hotstuffMetrics.BlockProcessingDuration(time.Since(startTime)) 321 }() 322 323 header := proposal.Header 324 blockID := header.ID() 325 326 span, ctx := c.tracer.StartBlockSpan(context.Background(), blockID, trace.ConCompProcessBlockProposal) 327 span.SetAttributes( 328 attribute.String("proposer", header.ProposerID.String()), 329 ) 330 defer span.End() 331 332 hotstuffProposal := model.ProposalFromFlow(header) 333 err := c.validator.ValidateProposal(hotstuffProposal) 334 if err != nil { 335 if model.IsInvalidProposalError(err) { 336 return err 337 } 338 if errors.Is(err, model.ErrViewForUnknownEpoch) { 339 // We have received a proposal, but we don't know the epoch its view is within. 340 // We know: 341 // - the parent of this block is valid and was appended to the state (ie. we knew the epoch for it) 342 // - if we then see this for the child, one of two things must have happened: 343 // 1. the proposer maliciously created the block for a view very far in the future (it's invalid) 344 // -> in this case we can disregard the block 345 // 2. no blocks have been finalized within the epoch commitment deadline, and the epoch ended 346 // (breaking a critical assumption - see EpochCommitSafetyThreshold in protocol.Params for details) 347 // -> in this case, the network has encountered a critical failure 348 // - we assume in general that Case 2 will not happen, therefore this must be Case 1 - an invalid block 349 return engine.NewUnverifiableInputError("unverifiable proposal with view from unknown epoch: %w", err) 350 } 351 return fmt.Errorf("unexpected error validating proposal: %w", err) 352 } 353 354 log := c.log.With(). 355 Str("chain_id", header.ChainID.String()). 356 Uint64("block_height", header.Height). 357 Uint64("block_view", header.View). 358 Hex("block_id", blockID[:]). 359 Hex("parent_id", header.ParentID[:]). 360 Hex("payload_hash", header.PayloadHash[:]). 361 Time("timestamp", header.Timestamp). 362 Hex("proposer", header.ProposerID[:]). 363 Hex("parent_signer_indices", header.ParentVoterIndices). 364 Logger() 365 log.Info().Msg("processing block proposal") 366 367 // see if the block is a valid extension of the protocol state 368 block := &flow.Block{ 369 Header: proposal.Header, 370 Payload: proposal.Payload, 371 } 372 err = c.state.Extend(ctx, block) 373 if err != nil { 374 if state.IsInvalidExtensionError(err) { 375 // if the block proposes an invalid extension of the protocol state, then the block is invalid 376 return model.NewInvalidProposalErrorf(hotstuffProposal, "invalid extension of protocol state (block: %x, height: %d): %w", blockID, header.Height, err) 377 } 378 if state.IsOutdatedExtensionError(err) { 379 // protocol state aborted processing of block as it is on an abandoned fork: block is outdated 380 return engine.NewOutdatedInputErrorf("outdated extension of protocol state: %w", err) 381 } 382 // unexpected error: potentially corrupted internal state => abort processing and escalate error 383 return fmt.Errorf("unexpected exception while extending protocol state with block %x at height %d: %w", blockID, header.Height, err) 384 } 385 386 // notify vote aggregator about a new block, so that it can start verifying 387 // votes for it. 388 c.voteAggregator.AddBlock(hotstuffProposal) 389 390 // submit the model to hotstuff for processing 391 // TODO replace with pubsub https://github.com/dapperlabs/flow-go/issues/6395 392 log.Info().Msg("forwarding block proposal to hotstuff") 393 c.hotstuff.SubmitProposal(hotstuffProposal) 394 395 return nil 396 } 397 398 // ProcessFinalizedBlock performs pruning of stale data based on finalization event 399 // removes pending blocks below the finalized view 400 func (c *Core) ProcessFinalizedBlock(finalized *flow.Header) { 401 // remove all pending blocks at or below the finalized view 402 c.pending.PruneByView(finalized.View) 403 c.finalizedHeight.Set(finalized.Height) 404 c.finalizedView.Set(finalized.View) 405 406 // always record the metric 407 c.mempoolMetrics.MempoolEntries(metrics.ResourceProposal, c.pending.Size()) 408 } 409 410 // checkForAndLogOutdatedInputError checks whether error is an `engine.OutdatedInputError`. 411 // If this is the case, we emit a log message and return true. 412 // For any error other than `engine.OutdatedInputError`, this function is a no-op 413 // and returns false. 414 func checkForAndLogOutdatedInputError(err error, log zerolog.Logger) bool { 415 if engine.IsOutdatedInputError(err) { 416 // child is outdated by the time we started processing it 417 // => node was probably behind and is catching up. Log as warning 418 log.Info().Msg("dropped processing of abandoned fork; this might be an indicator that the node is slightly behind") 419 return true 420 } 421 return false 422 } 423 424 // checkForAndLogUnverifiableInputError checks whether error is an `engine.UnverifiableInputError`. 425 // If this is the case, we emit a log message and return true. 426 // For any error other than `engine.UnverifiableInputError`, this function is a no-op 427 // and returns false. 428 func checkForAndLogUnverifiableInputError(err error, log zerolog.Logger) bool { 429 if engine.IsUnverifiableInputError(err) { 430 // the block cannot be validated 431 log.Warn().Err(err).Msg("received unverifiable block proposal; " + 432 "this might be an indicator that a malicious proposer is generating detached blocks very far ahead") 433 return true 434 } 435 return false 436 }