github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/state/cluster/badger/mutator.go (about) 1 package badger 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "math" 8 9 "github.com/dgraph-io/badger/v2" 10 11 "github.com/onflow/flow-go/model/cluster" 12 "github.com/onflow/flow-go/model/flow" 13 "github.com/onflow/flow-go/module" 14 "github.com/onflow/flow-go/module/irrecoverable" 15 "github.com/onflow/flow-go/module/trace" 16 "github.com/onflow/flow-go/state" 17 clusterstate "github.com/onflow/flow-go/state/cluster" 18 "github.com/onflow/flow-go/state/fork" 19 "github.com/onflow/flow-go/storage" 20 "github.com/onflow/flow-go/storage/badger/operation" 21 "github.com/onflow/flow-go/storage/badger/procedure" 22 ) 23 24 type MutableState struct { 25 *State 26 tracer module.Tracer 27 headers storage.Headers 28 payloads storage.ClusterPayloads 29 } 30 31 var _ clusterstate.MutableState = (*MutableState)(nil) 32 33 func NewMutableState(state *State, tracer module.Tracer, headers storage.Headers, payloads storage.ClusterPayloads) (*MutableState, error) { 34 mutableState := &MutableState{ 35 State: state, 36 tracer: tracer, 37 headers: headers, 38 payloads: payloads, 39 } 40 return mutableState, nil 41 } 42 43 // extendContext encapsulates all state information required in order to validate a candidate cluster block. 44 type extendContext struct { 45 candidate *cluster.Block // the proposed candidate cluster block 46 finalizedClusterBlock *flow.Header // the latest finalized cluster block 47 finalizedConsensusHeight uint64 // the latest finalized height on the main chain 48 epochFirstHeight uint64 // the first height of this cluster's operating epoch 49 epochLastHeight uint64 // the last height of this cluster's operating epoch (may be unknown) 50 epochHasEnded bool // whether this cluster's operating epoch has ended (whether the above field is known) 51 } 52 53 // getExtendCtx reads all required information from the database in order to validate 54 // a candidate cluster block. 55 // No errors are expected during normal operation. 56 func (m *MutableState) getExtendCtx(candidate *cluster.Block) (extendContext, error) { 57 var ctx extendContext 58 ctx.candidate = candidate 59 60 err := m.State.db.View(func(tx *badger.Txn) error { 61 // get the latest finalized cluster block and latest finalized consensus height 62 ctx.finalizedClusterBlock = new(flow.Header) 63 err := procedure.RetrieveLatestFinalizedClusterHeader(candidate.Header.ChainID, ctx.finalizedClusterBlock)(tx) 64 if err != nil { 65 return fmt.Errorf("could not retrieve finalized cluster head: %w", err) 66 } 67 err = operation.RetrieveFinalizedHeight(&ctx.finalizedConsensusHeight)(tx) 68 if err != nil { 69 return fmt.Errorf("could not retrieve finalized height on consensus chain: %w", err) 70 } 71 72 err = operation.RetrieveEpochFirstHeight(m.State.epoch, &ctx.epochFirstHeight)(tx) 73 if err != nil { 74 return fmt.Errorf("could not get operating epoch first height: %w", err) 75 } 76 err = operation.RetrieveEpochLastHeight(m.State.epoch, &ctx.epochLastHeight)(tx) 77 if err != nil { 78 if errors.Is(err, storage.ErrNotFound) { 79 ctx.epochHasEnded = false 80 return nil 81 } 82 return fmt.Errorf("unexpected failure to retrieve final height of operating epoch: %w", err) 83 } 84 ctx.epochHasEnded = true 85 return nil 86 }) 87 if err != nil { 88 return extendContext{}, fmt.Errorf("could not read required state information for Extend checks: %w", err) 89 } 90 return ctx, nil 91 } 92 93 // Extend introduces the given block into the cluster state as a pending 94 // without modifying the current finalized state. 95 // The block's parent must have already been successfully inserted. 96 // TODO(ramtin) pass context here 97 // Expected errors during normal operations: 98 // - state.OutdatedExtensionError if the candidate block is outdated (e.g. orphaned) 99 // - state.UnverifiableExtensionError if the reference block is _not_ a known finalized block 100 // - state.InvalidExtensionError if the candidate block is invalid 101 func (m *MutableState) Extend(candidate *cluster.Block) error { 102 parentSpan, ctx := m.tracer.StartCollectionSpan(context.Background(), candidate.ID(), trace.COLClusterStateMutatorExtend) 103 defer parentSpan.End() 104 105 span, _ := m.tracer.StartSpanFromContext(ctx, trace.COLClusterStateMutatorExtendCheckHeader) 106 err := m.checkHeaderValidity(candidate) 107 span.End() 108 if err != nil { 109 return fmt.Errorf("error checking header validity: %w", err) 110 } 111 112 span, _ = m.tracer.StartSpanFromContext(ctx, trace.COLClusterStateMutatorExtendGetExtendCtx) 113 extendCtx, err := m.getExtendCtx(candidate) 114 span.End() 115 if err != nil { 116 return fmt.Errorf("error gettting extend context data: %w", err) 117 } 118 119 span, _ = m.tracer.StartSpanFromContext(ctx, trace.COLClusterStateMutatorExtendCheckAncestry) 120 err = m.checkConnectsToFinalizedState(extendCtx) 121 span.End() 122 if err != nil { 123 return fmt.Errorf("error checking connection to finalized state: %w", err) 124 } 125 126 span, _ = m.tracer.StartSpanFromContext(ctx, trace.COLClusterStateMutatorExtendCheckReferenceBlock) 127 err = m.checkPayloadReferenceBlock(extendCtx) 128 span.End() 129 if err != nil { 130 return fmt.Errorf("error checking reference block: %w", err) 131 } 132 133 span, _ = m.tracer.StartSpanFromContext(ctx, trace.COLClusterStateMutatorExtendCheckTransactionsValid) 134 err = m.checkPayloadTransactions(extendCtx) 135 span.End() 136 if err != nil { 137 return fmt.Errorf("error checking payload transactions: %w", err) 138 } 139 140 span, _ = m.tracer.StartSpanFromContext(ctx, trace.COLClusterStateMutatorExtendDBInsert) 141 err = operation.RetryOnConflict(m.State.db.Update, procedure.InsertClusterBlock(candidate)) 142 span.End() 143 if err != nil { 144 return fmt.Errorf("could not insert cluster block: %w", err) 145 } 146 return nil 147 } 148 149 // checkHeaderValidity validates that the candidate block has a header which is 150 // valid generally for inclusion in the cluster consensus, and w.r.t. its parent. 151 // Expected error returns: 152 // - state.InvalidExtensionError if the candidate header is invalid 153 func (m *MutableState) checkHeaderValidity(candidate *cluster.Block) error { 154 header := candidate.Header 155 156 // check chain ID 157 if header.ChainID != m.State.clusterID { 158 return state.NewInvalidExtensionErrorf("new block chain ID (%s) does not match configured (%s)", header.ChainID, m.State.clusterID) 159 } 160 161 // get the header of the parent of the new block 162 parent, err := m.headers.ByBlockID(header.ParentID) 163 if err != nil { 164 return irrecoverable.NewExceptionf("could not retrieve latest finalized header: %w", err) 165 } 166 167 // extending block must have correct parent view 168 if header.ParentView != parent.View { 169 return state.NewInvalidExtensionErrorf("candidate build with inconsistent parent view (candidate: %d, parent %d)", 170 header.ParentView, parent.View) 171 } 172 173 // the extending block must increase height by 1 from parent 174 if header.Height != parent.Height+1 { 175 return state.NewInvalidExtensionErrorf("extending block height (%d) must be parent height + 1 (%d)", 176 header.Height, parent.Height) 177 } 178 return nil 179 } 180 181 // checkConnectsToFinalizedState validates that the candidate block connects to 182 // the latest finalized state (ie. is not extending an orphaned fork). 183 // Expected error returns: 184 // - state.OutdatedExtensionError if the candidate extends an orphaned fork 185 func (m *MutableState) checkConnectsToFinalizedState(ctx extendContext) error { 186 header := ctx.candidate.Header 187 finalizedID := ctx.finalizedClusterBlock.ID() 188 finalizedHeight := ctx.finalizedClusterBlock.Height 189 190 // start with the extending block's parent 191 parentID := header.ParentID 192 for parentID != finalizedID { 193 // get the parent of current block 194 ancestor, err := m.headers.ByBlockID(parentID) 195 if err != nil { 196 return irrecoverable.NewExceptionf("could not get parent which must be known (%x): %w", header.ParentID, err) 197 } 198 199 // if its height is below current boundary, the block does not connect 200 // to the finalized protocol state and would break database consistency 201 if ancestor.Height < finalizedHeight { 202 return state.NewOutdatedExtensionErrorf( 203 "block doesn't connect to latest finalized block (height=%d, id=%x): orphaned ancestor (height=%d, id=%x)", 204 finalizedHeight, finalizedID, ancestor.Height, parentID) 205 } 206 parentID = ancestor.ParentID 207 } 208 return nil 209 } 210 211 // checkPayloadReferenceBlock validates the reference block is valid. 212 // - it must be a known, finalized block on the main consensus chain 213 // - it must be within the cluster's operating epoch 214 // 215 // Expected error returns: 216 // - state.InvalidExtensionError if the reference block is invalid for use. 217 // - state.UnverifiableExtensionError if the reference block is unknown. 218 func (m *MutableState) checkPayloadReferenceBlock(ctx extendContext) error { 219 payload := ctx.candidate.Payload 220 221 // 1 - the reference block must be known 222 refBlock, err := m.headers.ByBlockID(payload.ReferenceBlockID) 223 if err != nil { 224 if errors.Is(err, storage.ErrNotFound) { 225 return state.NewUnverifiableExtensionError("cluster block references unknown reference block (id=%x)", payload.ReferenceBlockID) 226 } 227 return fmt.Errorf("could not check reference block: %w", err) 228 } 229 230 // 2 - the reference block must be finalized 231 if refBlock.Height > ctx.finalizedConsensusHeight { 232 // a reference block which is above the finalized boundary can't be verified yet 233 return state.NewUnverifiableExtensionError("reference block is above finalized boundary (%d>%d)", refBlock.Height, ctx.finalizedConsensusHeight) 234 } else { 235 storedBlockIDForHeight, err := m.headers.BlockIDByHeight(refBlock.Height) 236 if err != nil { 237 return irrecoverable.NewExceptionf("could not look up block ID for finalized height: %w", err) 238 } 239 // a reference block with height at or below the finalized boundary must have been finalized 240 if storedBlockIDForHeight != payload.ReferenceBlockID { 241 return state.NewInvalidExtensionErrorf("cluster block references orphaned reference block (id=%x, height=%d), the block finalized at this height is %x", 242 payload.ReferenceBlockID, refBlock.Height, storedBlockIDForHeight) 243 } 244 } 245 246 // TODO ensure the reference block is part of the main chain https://github.com/onflow/flow-go/issues/4204 247 _ = refBlock 248 249 // 3 - the reference block must be within the cluster's operating epoch 250 if refBlock.Height < ctx.epochFirstHeight { 251 return state.NewInvalidExtensionErrorf("invalid reference block is before operating epoch for cluster, height %d<%d", refBlock.Height, ctx.epochFirstHeight) 252 } 253 if ctx.epochHasEnded && refBlock.Height > ctx.epochLastHeight { 254 return state.NewInvalidExtensionErrorf("invalid reference block is after operating epoch for cluster, height %d>%d", refBlock.Height, ctx.epochLastHeight) 255 } 256 return nil 257 } 258 259 // checkPayloadTransactions validates the transactions included int the candidate cluster block's payload. 260 // It enforces: 261 // - transactions are individually valid 262 // - no duplicate transaction exists along the fork being extended 263 // - the collection's reference block is equal to the oldest reference block among 264 // its constituent transactions 265 // 266 // Expected error returns: 267 // - state.InvalidExtensionError if the reference block is invalid for use. 268 // - state.UnverifiableExtensionError if the reference block is unknown. 269 func (m *MutableState) checkPayloadTransactions(ctx extendContext) error { 270 block := ctx.candidate 271 payload := block.Payload 272 273 if payload.Collection.Len() == 0 { 274 return nil 275 } 276 277 // check that all transactions within the collection are valid 278 // keep track of the min/max reference blocks - the collection must be non-empty 279 // at this point so these are guaranteed to be set correctly 280 minRefID := flow.ZeroID 281 minRefHeight := uint64(math.MaxUint64) 282 maxRefHeight := uint64(0) 283 for _, flowTx := range payload.Collection.Transactions { 284 refBlock, err := m.headers.ByBlockID(flowTx.ReferenceBlockID) 285 if errors.Is(err, storage.ErrNotFound) { 286 // unknown reference blocks are invalid 287 return state.NewUnverifiableExtensionError("collection contains tx (tx_id=%x) with unknown reference block (block_id=%x): %w", flowTx.ID(), flowTx.ReferenceBlockID, err) 288 } 289 if err != nil { 290 return fmt.Errorf("could not check reference block (id=%x): %w", flowTx.ReferenceBlockID, err) 291 } 292 293 if refBlock.Height < minRefHeight { 294 minRefHeight = refBlock.Height 295 minRefID = flowTx.ReferenceBlockID 296 } 297 if refBlock.Height > maxRefHeight { 298 maxRefHeight = refBlock.Height 299 } 300 } 301 302 // a valid collection must reference the oldest reference block among 303 // its constituent transactions 304 if minRefID != payload.ReferenceBlockID { 305 return state.NewInvalidExtensionErrorf( 306 "reference block (id=%x) must match oldest transaction's reference block (id=%x)", 307 payload.ReferenceBlockID, minRefID, 308 ) 309 } 310 // a valid collection must contain only transactions within its expiry window 311 if maxRefHeight-minRefHeight >= flow.DefaultTransactionExpiry { 312 return state.NewInvalidExtensionErrorf( 313 "collection contains reference height range [%d,%d] exceeding expiry window size: %d", 314 minRefHeight, maxRefHeight, flow.DefaultTransactionExpiry) 315 } 316 317 // check for duplicate transactions in block's ancestry 318 txLookup := make(map[flow.Identifier]struct{}) 319 for _, tx := range block.Payload.Collection.Transactions { 320 txID := tx.ID() 321 if _, exists := txLookup[txID]; exists { 322 return state.NewInvalidExtensionErrorf("collection contains transaction (id=%x) more than once", txID) 323 } 324 txLookup[txID] = struct{}{} 325 } 326 327 // first, check for duplicate transactions in the un-finalized ancestry 328 duplicateTxIDs, err := m.checkDupeTransactionsInUnfinalizedAncestry(block, txLookup, ctx.finalizedClusterBlock.Height) 329 if err != nil { 330 return fmt.Errorf("could not check for duplicate txs in un-finalized ancestry: %w", err) 331 } 332 if len(duplicateTxIDs) > 0 { 333 return state.NewInvalidExtensionErrorf("payload includes duplicate transactions in un-finalized ancestry (duplicates: %s)", duplicateTxIDs) 334 } 335 336 // second, check for duplicate transactions in the finalized ancestry 337 duplicateTxIDs, err = m.checkDupeTransactionsInFinalizedAncestry(txLookup, minRefHeight, maxRefHeight) 338 if err != nil { 339 return fmt.Errorf("could not check for duplicate txs in finalized ancestry: %w", err) 340 } 341 if len(duplicateTxIDs) > 0 { 342 return state.NewInvalidExtensionErrorf("payload includes duplicate transactions in finalized ancestry (duplicates: %s)", duplicateTxIDs) 343 } 344 345 return nil 346 } 347 348 // checkDupeTransactionsInUnfinalizedAncestry checks for duplicate transactions in the un-finalized 349 // ancestry of the given block, and returns a list of all duplicates if there are any. 350 func (m *MutableState) checkDupeTransactionsInUnfinalizedAncestry(block *cluster.Block, includedTransactions map[flow.Identifier]struct{}, finalHeight uint64) ([]flow.Identifier, error) { 351 352 var duplicateTxIDs []flow.Identifier 353 err := fork.TraverseBackward(m.headers, block.Header.ParentID, func(ancestor *flow.Header) error { 354 payload, err := m.payloads.ByBlockID(ancestor.ID()) 355 if err != nil { 356 return fmt.Errorf("could not retrieve ancestor payload: %w", err) 357 } 358 359 for _, tx := range payload.Collection.Transactions { 360 txID := tx.ID() 361 _, duplicated := includedTransactions[txID] 362 if duplicated { 363 duplicateTxIDs = append(duplicateTxIDs, txID) 364 } 365 } 366 return nil 367 }, fork.ExcludingHeight(finalHeight)) 368 369 return duplicateTxIDs, err 370 } 371 372 // checkDupeTransactionsInFinalizedAncestry checks for duplicate transactions in the finalized 373 // ancestry, and returns a list of all duplicates if there are any. 374 func (m *MutableState) checkDupeTransactionsInFinalizedAncestry(includedTransactions map[flow.Identifier]struct{}, minRefHeight, maxRefHeight uint64) ([]flow.Identifier, error) { 375 var duplicatedTxIDs []flow.Identifier 376 377 // Let E be the global transaction expiry constant, measured in blocks. For each 378 // T ∈ `includedTransactions`, we have to decide whether the transaction 379 // already appeared in _any_ finalized cluster block. 380 // Notation: 381 // - consider a valid cluster block C and let c be its reference block height 382 // - consider a transaction T ∈ `includedTransactions` and let t denote its 383 // reference block height 384 // 385 // Boundary conditions: 386 // 1. C's reference block height is equal to the lowest reference block height of 387 // all its constituent transactions. Hence, for collection C to potentially contain T, it must satisfy c <= t. 388 // 2. For T to be eligible for inclusion in collection C, _none_ of the transactions within C are allowed 389 // to be expired w.r.t. C's reference block. Hence, for collection C to potentially contain T, it must satisfy t < c + E. 390 // 391 // Therefore, for collection C to potentially contain transaction T, it must satisfy t - E < c <= t. 392 // In other words, we only need to inspect collections with reference block height c ∈ (t-E, t]. 393 // Consequently, for a set of transactions, with `minRefHeight` (`maxRefHeight`) being the smallest (largest) 394 // reference block height, we only need to inspect collections with c ∈ (minRefHeight-E, maxRefHeight]. 395 396 // the finalized cluster blocks which could possibly contain any conflicting transactions 397 var clusterBlockIDs []flow.Identifier 398 start := minRefHeight - flow.DefaultTransactionExpiry + 1 399 if start > minRefHeight { 400 start = 0 // overflow check 401 } 402 end := maxRefHeight 403 err := m.db.View(operation.LookupClusterBlocksByReferenceHeightRange(start, end, &clusterBlockIDs)) 404 if err != nil { 405 return nil, fmt.Errorf("could not lookup finalized cluster blocks by reference height range [%d,%d]: %w", start, end, err) 406 } 407 408 for _, blockID := range clusterBlockIDs { 409 // TODO: could add LightByBlockID and retrieve only tx IDs 410 payload, err := m.payloads.ByBlockID(blockID) 411 if err != nil { 412 return nil, fmt.Errorf("could not retrieve cluster payload (block_id=%x) to de-duplicate: %w", blockID, err) 413 } 414 for _, tx := range payload.Collection.Transactions { 415 txID := tx.ID() 416 _, duplicated := includedTransactions[txID] 417 if duplicated { 418 duplicatedTxIDs = append(duplicatedTxIDs, txID) 419 } 420 } 421 } 422 423 return duplicatedTxIDs, nil 424 }