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 }