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 }