github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/engine/consensus/ingestion/core.go (about)

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