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  }