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  }