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  }