github.com/onflow/flow-go@v0.33.17/engine/consensus/ingestion/core.go (about)

     1  // (c) 2019 Dapper Labs - ALL RIGHTS RESERVED
     2  
     3  package ingestion
     4  
     5  import (
     6  	"context"
     7  	"errors"
     8  	"fmt"
     9  
    10  	"github.com/rs/zerolog"
    11  	"go.opentelemetry.io/otel/attribute"
    12  
    13  	"github.com/onflow/flow-go/consensus/hotstuff/committees"
    14  	"github.com/onflow/flow-go/engine"
    15  	"github.com/onflow/flow-go/model/flow"
    16  	"github.com/onflow/flow-go/module"
    17  	"github.com/onflow/flow-go/module/mempool"
    18  	"github.com/onflow/flow-go/module/metrics"
    19  	"github.com/onflow/flow-go/module/signature"
    20  	"github.com/onflow/flow-go/module/trace"
    21  	"github.com/onflow/flow-go/state"
    22  	"github.com/onflow/flow-go/state/protocol"
    23  	"github.com/onflow/flow-go/storage"
    24  )
    25  
    26  // Core represents core logic of the ingestion engine. It contains logic
    27  // for handling single collection which are channeled from engine in concurrent way.
    28  type Core struct {
    29  	log     zerolog.Logger        // used to log relevant actions with context
    30  	tracer  module.Tracer         // used for tracing
    31  	mempool module.MempoolMetrics // used to track mempool metrics
    32  	state   protocol.State        // used to access the protocol state
    33  	headers storage.Headers       // used to retrieve headers
    34  	pool    mempool.Guarantees    // used to keep pending guarantees in pool
    35  }
    36  
    37  func NewCore(
    38  	log zerolog.Logger,
    39  	tracer module.Tracer,
    40  	mempool module.MempoolMetrics,
    41  	state protocol.State,
    42  	headers storage.Headers,
    43  	pool mempool.Guarantees,
    44  ) *Core {
    45  	return &Core{
    46  		log:     log.With().Str("ingestion", "core").Logger(),
    47  		tracer:  tracer,
    48  		mempool: mempool,
    49  		state:   state,
    50  		headers: headers,
    51  		pool:    pool,
    52  	}
    53  }
    54  
    55  // OnGuarantee is used to process collection guarantees received
    56  // from nodes that are not consensus nodes (notably collection nodes).
    57  // Returns expected errors:
    58  //   - engine.InvalidInputError if the collection violates protocol rules
    59  //   - engine.UnverifiableInputError if the reference block of the collection is unknown
    60  //   - engine.OutdatedInputError if the collection is already expired
    61  //
    62  // All other errors are unexpected and potential symptoms of internal state corruption.
    63  func (e *Core) OnGuarantee(originID flow.Identifier, guarantee *flow.CollectionGuarantee) error {
    64  
    65  	span, _ := e.tracer.StartCollectionSpan(context.Background(), guarantee.CollectionID, trace.CONIngOnCollectionGuarantee)
    66  	span.SetAttributes(
    67  		attribute.String("originID", originID.String()),
    68  	)
    69  	defer span.End()
    70  
    71  	guaranteeID := guarantee.ID()
    72  
    73  	log := e.log.With().
    74  		Hex("origin_id", originID[:]).
    75  		Hex("collection_id", guaranteeID[:]).
    76  		Hex("signers", guarantee.SignerIndices).
    77  		Logger()
    78  	log.Info().Msg("collection guarantee received")
    79  
    80  	// skip collection guarantees that are already in our memory pool
    81  	exists := e.pool.Has(guaranteeID)
    82  	if exists {
    83  		log.Debug().Msg("skipping known collection guarantee")
    84  		return nil
    85  	}
    86  
    87  	// check collection guarantee's validity
    88  	err := e.validateOrigin(originID, guarantee) // retrieve and validate the sender of the collection guarantee
    89  	if err != nil {
    90  		return fmt.Errorf("origin validation error: %w", err)
    91  	}
    92  	err = e.validateExpiry(guarantee) // ensure that collection has not expired
    93  	if err != nil {
    94  		return fmt.Errorf("expiry validation error: %w", err)
    95  	}
    96  	err = e.validateGuarantors(guarantee) // ensure the guarantors are allowed to produce this collection
    97  	if err != nil {
    98  		return fmt.Errorf("guarantor validation error: %w", err)
    99  	}
   100  
   101  	// at this point, we can add the guarantee to the memory pool
   102  	added := e.pool.Add(guarantee)
   103  	if !added {
   104  		log.Debug().Msg("discarding guarantee already in pool")
   105  		return nil
   106  	}
   107  	log.Info().Msg("collection guarantee added to pool")
   108  
   109  	e.mempool.MempoolEntries(metrics.ResourceGuarantee, e.pool.Size())
   110  	return nil
   111  }
   112  
   113  // validateExpiry validates that the collection has not expired w.r.t. the local
   114  // latest finalized block.
   115  // Expected errors during normal operation:
   116  //   - engine.UnverifiableInputError if the reference block of the collection is unknown
   117  //   - engine.OutdatedInputError if the collection is already expired
   118  //
   119  // All other errors are unexpected and potential symptoms of internal state corruption.
   120  func (e *Core) validateExpiry(guarantee *flow.CollectionGuarantee) error {
   121  	// get the last finalized header and the reference block header
   122  	final, err := e.state.Final().Head()
   123  	if err != nil {
   124  		return fmt.Errorf("could not get finalized header: %w", err)
   125  	}
   126  	ref, err := e.headers.ByBlockID(guarantee.ReferenceBlockID)
   127  	if errors.Is(err, storage.ErrNotFound) {
   128  		return engine.NewUnverifiableInputError("collection guarantee refers to an unknown block (id=%x): %w", guarantee.ReferenceBlockID, err)
   129  	}
   130  
   131  	// if head has advanced beyond the block referenced by the collection guarantee by more than 'expiry' number of blocks,
   132  	// then reject the collection
   133  	if ref.Height > final.Height {
   134  		return nil // the reference block is newer than the latest finalized one
   135  	}
   136  	if final.Height-ref.Height > flow.DefaultTransactionExpiry {
   137  		return engine.NewOutdatedInputErrorf("collection guarantee expired ref_height=%d final_height=%d", ref.Height, final.Height)
   138  	}
   139  
   140  	return nil
   141  }
   142  
   143  // validateGuarantors validates that the guarantors of a collection are valid,
   144  // in that they are all from the same cluster and that cluster is allowed to
   145  // produce the given collection w.r.t. the guarantee's reference block.
   146  // Expected errors during normal operation:
   147  //   - engine.InvalidInputError if the origin violates any requirements
   148  //   - engine.UnverifiableInputError if the reference block of the collection is unknown
   149  //
   150  // All other errors are unexpected and potential symptoms of internal state corruption.
   151  //
   152  // TODO: Eventually we should check the signatures, ensure a quorum of the
   153  // cluster, and ensure HotStuff finalization rules. Likely a cluster-specific
   154  // version of the follower will be a good fit for this. For now, collection
   155  // nodes independently decide when a collection is finalized and we only check
   156  // that the guarantors are all from the same cluster. This implementation is NOT BFT.
   157  func (e *Core) validateGuarantors(guarantee *flow.CollectionGuarantee) error {
   158  	// get the clusters to assign the guarantee and check if the guarantor is part of it
   159  	snapshot := e.state.AtBlockID(guarantee.ReferenceBlockID)
   160  	cluster, err := snapshot.Epochs().Current().ClusterByChainID(guarantee.ChainID)
   161  	// reference block not found
   162  	if errors.Is(err, state.ErrUnknownSnapshotReference) {
   163  		return engine.NewUnverifiableInputError(
   164  			"could not get clusters with chainID %v for unknown reference block (id=%x): %w", guarantee.ChainID, guarantee.ReferenceBlockID, err)
   165  	}
   166  	// cluster not found by the chain ID
   167  	if errors.Is(err, protocol.ErrClusterNotFound) {
   168  		return engine.NewInvalidInputErrorf("cluster not found by chain ID %v: %w", guarantee.ChainID, err)
   169  	}
   170  	if err != nil {
   171  		return fmt.Errorf("internal error retrieving collector clusters for guarantee (ReferenceBlockID: %v, ChainID: %v): %w",
   172  			guarantee.ReferenceBlockID, guarantee.ChainID, err)
   173  	}
   174  
   175  	// ensure the guarantors are from the same cluster
   176  	clusterMembers := cluster.Members()
   177  
   178  	// find guarantors by signer indices
   179  	guarantors, err := signature.DecodeSignerIndicesToIdentities(clusterMembers, guarantee.SignerIndices)
   180  	if err != nil {
   181  		if signature.IsInvalidSignerIndicesError(err) {
   182  			return engine.NewInvalidInputErrorf("could not decode guarantor indices: %w", err)
   183  		}
   184  		// unexpected error
   185  		return fmt.Errorf("unexpected internal error decoding signer indices: %w", err)
   186  	}
   187  
   188  	// determine whether signers reach minimally required stake threshold
   189  	threshold := committees.WeightThresholdToBuildQC(clusterMembers.TotalWeight()) // compute required stake threshold
   190  	totalStake := flow.IdentityList(guarantors).TotalWeight()
   191  	if totalStake < threshold {
   192  		return engine.NewInvalidInputErrorf("collection guarantee qc signers have insufficient stake of %d (required=%d)", totalStake, threshold)
   193  	}
   194  
   195  	return nil
   196  }
   197  
   198  // validateOrigin validates that the message has a valid sender (origin). We
   199  // only accept guarantees from an origin that is part of the identity table
   200  // at the collection's reference block. Furthermore, the origin must be
   201  // an authorized (i.e. positive weight), non-ejected collector node.
   202  // Expected errors during normal operation:
   203  //   - engine.InvalidInputError if the origin violates any requirements
   204  //   - engine.UnverifiableInputError if the reference block of the collection is unknown
   205  //
   206  // All other errors are unexpected and potential symptoms of internal state corruption.
   207  //
   208  // TODO: ultimately, the origin broadcasting a collection is irrelevant, as long as the
   209  // collection itself is valid. The origin is only needed in case the guarantee is found
   210  // to be invalid, in which case we might want to slash the origin.
   211  func (e *Core) validateOrigin(originID flow.Identifier, guarantee *flow.CollectionGuarantee) error {
   212  	refState := e.state.AtBlockID(guarantee.ReferenceBlockID)
   213  	valid, err := protocol.IsNodeAuthorizedWithRoleAt(refState, originID, flow.RoleCollection)
   214  	if err != nil {
   215  		// collection with an unknown reference block is unverifiable
   216  		if errors.Is(err, state.ErrUnknownSnapshotReference) {
   217  			return engine.NewUnverifiableInputError("could not get origin (id=%x) for unknown reference block (id=%x): %w", originID, guarantee.ReferenceBlockID, err)
   218  		}
   219  		return fmt.Errorf("unexpected error checking collection origin %x at reference block %x: %w", originID, guarantee.ReferenceBlockID, err)
   220  	}
   221  	if !valid {
   222  		return engine.NewInvalidInputErrorf("invalid collection origin (id=%x)", originID)
   223  	}
   224  	return nil
   225  }