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