github.com/onflow/flow-go@v0.33.17/engine/access/rpc/backend/transactions_local_data_provider.go (about)

     1  package backend
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  
     8  	"google.golang.org/grpc/status"
     9  
    10  	"github.com/onflow/flow/protobuf/go/flow/entities"
    11  	"google.golang.org/grpc/codes"
    12  
    13  	"github.com/onflow/flow-go/access"
    14  	"github.com/onflow/flow-go/engine/common/rpc"
    15  	"github.com/onflow/flow-go/engine/common/rpc/convert"
    16  	"github.com/onflow/flow-go/model/flow"
    17  	"github.com/onflow/flow-go/module/irrecoverable"
    18  	"github.com/onflow/flow-go/state"
    19  	"github.com/onflow/flow-go/state/protocol"
    20  	"github.com/onflow/flow-go/storage"
    21  )
    22  
    23  // TransactionErrorMessage declares the lookup transaction error methods by different input parameters.
    24  type TransactionErrorMessage interface {
    25  	// LookupErrorMessageByTransactionID is a function type for getting transaction error message by block ID and transaction ID.
    26  	// Expected errors during normal operation:
    27  	//   - InsufficientExecutionReceipts - found insufficient receipts for given block ID.
    28  	//   - status.Error - remote GRPC call to EN has failed.
    29  	LookupErrorMessageByTransactionID(ctx context.Context, blockID flow.Identifier, transactionID flow.Identifier) (string, error)
    30  
    31  	// LookupErrorMessageByIndex is a function type for getting transaction error message by index.
    32  	// Expected errors during normal operation:
    33  	//   - status.Error[codes.NotFound] - transaction result for given block ID and tx index is not available.
    34  	//   - InsufficientExecutionReceipts - found insufficient receipts for given block ID.
    35  	//   - status.Error - remote GRPC call to EN has failed.
    36  	LookupErrorMessageByIndex(ctx context.Context, blockID flow.Identifier, height uint64, index uint32) (string, error)
    37  
    38  	// LookupErrorMessagesByBlockID is a function type for getting transaction error messages by block ID.
    39  	// Expected errors during normal operation:
    40  	//   - status.Error[codes.NotFound] - transaction results for given block ID are not available.
    41  	//   - InsufficientExecutionReceipts - found insufficient receipts for given block ID.
    42  	//   - status.Error - remote GRPC call to EN has failed.
    43  	LookupErrorMessagesByBlockID(ctx context.Context, blockID flow.Identifier, height uint64) (map[flow.Identifier]string, error)
    44  }
    45  
    46  // TransactionsLocalDataProvider provides functionality for retrieving transaction results and error messages from local storages
    47  type TransactionsLocalDataProvider struct {
    48  	state           protocol.State
    49  	collections     storage.Collections
    50  	blocks          storage.Blocks
    51  	eventsIndex     *EventsIndex
    52  	txResultsIndex  *TransactionResultsIndex
    53  	txErrorMessages TransactionErrorMessage
    54  	systemTxID      flow.Identifier
    55  }
    56  
    57  // GetTransactionResultFromStorage retrieves a transaction result from storage by block ID and transaction ID.
    58  // Expected errors during normal operation:
    59  //   - codes.NotFound when result cannot be provided by storage due to the absence of data.
    60  //   - codes.Internal if event payload conversion failed.
    61  //   - indexer.ErrIndexNotInitialized when txResultsIndex not initialized
    62  //   - storage.ErrHeightNotIndexed when data is unavailable
    63  //
    64  // All other errors are considered as state corruption (fatal) or internal errors in the transaction error message
    65  // getter or when deriving transaction status.
    66  func (t *TransactionsLocalDataProvider) GetTransactionResultFromStorage(
    67  	ctx context.Context,
    68  	block *flow.Block,
    69  	transactionID flow.Identifier,
    70  	requiredEventEncodingVersion entities.EventEncodingVersion,
    71  ) (*access.TransactionResult, error) {
    72  	blockID := block.ID()
    73  	txResult, err := t.txResultsIndex.ByBlockIDTransactionID(blockID, block.Header.Height, transactionID)
    74  	if err != nil {
    75  		return nil, rpc.ConvertIndexError(err, block.Header.Height, "failed to get transaction result")
    76  	}
    77  
    78  	var txErrorMessage string
    79  	var txStatusCode uint = 0
    80  	if txResult.Failed {
    81  		txErrorMessage, err = t.txErrorMessages.LookupErrorMessageByTransactionID(ctx, blockID, transactionID)
    82  		if err != nil {
    83  			return nil, err
    84  		}
    85  
    86  		if len(txErrorMessage) == 0 {
    87  			return nil, status.Errorf(codes.Internal, "transaction failed but error message is empty for tx ID: %s block ID: %s", txResult.TransactionID, blockID)
    88  		}
    89  
    90  		txStatusCode = 1 // statusCode of 1 indicates an error and 0 indicates no error, the same as on EN
    91  	}
    92  
    93  	txStatus, err := t.deriveTransactionStatus(blockID, block.Header.Height, true)
    94  	if err != nil {
    95  		if !errors.Is(err, state.ErrUnknownSnapshotReference) {
    96  			irrecoverable.Throw(ctx, err)
    97  		}
    98  		return nil, rpc.ConvertStorageError(err)
    99  	}
   100  
   101  	events, err := t.eventsIndex.ByBlockIDTransactionID(blockID, block.Header.Height, transactionID)
   102  	if err != nil {
   103  		return nil, rpc.ConvertIndexError(err, block.Header.Height, "failed to get events")
   104  	}
   105  
   106  	// events are encoded in CCF format in storage. convert to JSON-CDC if requested
   107  	if requiredEventEncodingVersion == entities.EventEncodingVersion_JSON_CDC_V0 {
   108  		events, err = convert.CcfEventsToJsonEvents(events)
   109  		if err != nil {
   110  			return nil, rpc.ConvertError(err, "failed to convert event payload", codes.Internal)
   111  		}
   112  	}
   113  
   114  	return &access.TransactionResult{
   115  		TransactionID: txResult.TransactionID,
   116  		Status:        txStatus,
   117  		StatusCode:    txStatusCode,
   118  		Events:        events,
   119  		ErrorMessage:  txErrorMessage,
   120  		BlockID:       blockID,
   121  		BlockHeight:   block.Header.Height,
   122  	}, nil
   123  }
   124  
   125  // GetTransactionResultsByBlockIDFromStorage retrieves transaction results by block ID from storage
   126  // Expected errors during normal operation:
   127  //   - codes.NotFound if result cannot be provided by storage due to the absence of data.
   128  //   - codes.Internal when event payload conversion failed.
   129  //   - indexer.ErrIndexNotInitialized when txResultsIndex not initialized
   130  //   - storage.ErrHeightNotIndexed when data is unavailable
   131  //
   132  // All other errors are considered as state corruption (fatal) or internal errors in the transaction error message
   133  // getter or when deriving transaction status.
   134  func (t *TransactionsLocalDataProvider) GetTransactionResultsByBlockIDFromStorage(
   135  	ctx context.Context,
   136  	block *flow.Block,
   137  	requiredEventEncodingVersion entities.EventEncodingVersion,
   138  ) ([]*access.TransactionResult, error) {
   139  	blockID := block.ID()
   140  	txResults, err := t.txResultsIndex.ByBlockID(blockID, block.Header.Height)
   141  	if err != nil {
   142  		return nil, rpc.ConvertIndexError(err, block.Header.Height, "failed to get transaction result")
   143  	}
   144  
   145  	txErrors, err := t.txErrorMessages.LookupErrorMessagesByBlockID(ctx, blockID, block.Header.Height)
   146  	if err != nil {
   147  		return nil, err
   148  	}
   149  
   150  	numberOfTxResults := len(txResults)
   151  	results := make([]*access.TransactionResult, 0, numberOfTxResults)
   152  
   153  	// cache the tx to collectionID mapping to avoid repeated lookups
   154  	txToCollectionID, err := t.buildTxIDToCollectionIDMapping(block)
   155  	if err != nil {
   156  		// this indicates that one or more of the collections for the block are not indexed. Since
   157  		// lookups are gated on the indexer signaling it has finished processing all data for the
   158  		// block, all data must be available in storage, otherwise there is an inconsistency in the
   159  		// state.
   160  		irrecoverable.Throw(ctx, fmt.Errorf("inconsistent index state: %w", err))
   161  		return nil, status.Errorf(codes.Internal, "failed to map tx to collection ID: %v", err)
   162  	}
   163  
   164  	for _, txResult := range txResults {
   165  		txID := txResult.TransactionID
   166  
   167  		var txErrorMessage string
   168  		var txStatusCode uint = 0
   169  		if txResult.Failed {
   170  			txErrorMessage = txErrors[txResult.TransactionID]
   171  			if len(txErrorMessage) == 0 {
   172  				return nil, status.Errorf(codes.Internal, "transaction failed but error message is empty for tx ID: %s block ID: %s", txID, blockID)
   173  			}
   174  			txStatusCode = 1
   175  		}
   176  
   177  		txStatus, err := t.deriveTransactionStatus(blockID, block.Header.Height, true)
   178  		if err != nil {
   179  			if !errors.Is(err, state.ErrUnknownSnapshotReference) {
   180  				irrecoverable.Throw(ctx, err)
   181  			}
   182  			return nil, rpc.ConvertStorageError(err)
   183  		}
   184  
   185  		events, err := t.eventsIndex.ByBlockIDTransactionID(blockID, block.Header.Height, txResult.TransactionID)
   186  		if err != nil {
   187  			return nil, rpc.ConvertIndexError(err, block.Header.Height, "failed to get events")
   188  		}
   189  
   190  		// events are encoded in CCF format in storage. convert to JSON-CDC if requested
   191  		if requiredEventEncodingVersion == entities.EventEncodingVersion_JSON_CDC_V0 {
   192  			events, err = convert.CcfEventsToJsonEvents(events)
   193  			if err != nil {
   194  				return nil, rpc.ConvertError(err, "failed to convert event payload", codes.Internal)
   195  			}
   196  		}
   197  
   198  		collectionID, ok := txToCollectionID[txID]
   199  		if !ok {
   200  			return nil, status.Errorf(codes.Internal, "transaction %s not found in block %s", txID, blockID)
   201  		}
   202  
   203  		results = append(results, &access.TransactionResult{
   204  			Status:        txStatus,
   205  			StatusCode:    txStatusCode,
   206  			Events:        events,
   207  			ErrorMessage:  txErrorMessage,
   208  			BlockID:       blockID,
   209  			TransactionID: txID,
   210  			CollectionID:  collectionID,
   211  			BlockHeight:   block.Header.Height,
   212  		})
   213  	}
   214  
   215  	return results, nil
   216  }
   217  
   218  // GetTransactionResultByIndexFromStorage retrieves a transaction result by index from storage.
   219  // Expected errors during normal operation:
   220  //   - codes.NotFound if result cannot be provided by storage due to the absence of data.
   221  //   - codes.Internal when event payload conversion failed.
   222  //   - indexer.ErrIndexNotInitialized when txResultsIndex not initialized
   223  //   - storage.ErrHeightNotIndexed when data is unavailable
   224  //
   225  // All other errors are considered as state corruption (fatal) or internal errors in the transaction error message
   226  // getter or when deriving transaction status.
   227  func (t *TransactionsLocalDataProvider) GetTransactionResultByIndexFromStorage(
   228  	ctx context.Context,
   229  	block *flow.Block,
   230  	index uint32,
   231  	requiredEventEncodingVersion entities.EventEncodingVersion,
   232  ) (*access.TransactionResult, error) {
   233  	blockID := block.ID()
   234  	txResult, err := t.txResultsIndex.ByBlockIDTransactionIndex(blockID, block.Header.Height, index)
   235  	if err != nil {
   236  		return nil, rpc.ConvertIndexError(err, block.Header.Height, "failed to get transaction result")
   237  	}
   238  
   239  	var txErrorMessage string
   240  	var txStatusCode uint = 0
   241  	if txResult.Failed {
   242  		txErrorMessage, err = t.txErrorMessages.LookupErrorMessageByIndex(ctx, blockID, block.Header.Height, index)
   243  		if err != nil {
   244  			return nil, err
   245  		}
   246  
   247  		if len(txErrorMessage) == 0 {
   248  			return nil, status.Errorf(codes.Internal, "transaction failed but error message is empty for tx ID: %s block ID: %s", txResult.TransactionID, blockID)
   249  		}
   250  
   251  		txStatusCode = 1 // statusCode of 1 indicates an error and 0 indicates no error, the same as on EN
   252  	}
   253  
   254  	txStatus, err := t.deriveTransactionStatus(blockID, block.Header.Height, true)
   255  	if err != nil {
   256  		if !errors.Is(err, state.ErrUnknownSnapshotReference) {
   257  			irrecoverable.Throw(ctx, err)
   258  		}
   259  		return nil, rpc.ConvertStorageError(err)
   260  	}
   261  
   262  	events, err := t.eventsIndex.ByBlockIDTransactionIndex(blockID, block.Header.Height, index)
   263  	if err != nil {
   264  		return nil, rpc.ConvertIndexError(err, block.Header.Height, "failed to get events")
   265  	}
   266  
   267  	// events are encoded in CCF format in storage. convert to JSON-CDC if requested
   268  	if requiredEventEncodingVersion == entities.EventEncodingVersion_JSON_CDC_V0 {
   269  		events, err = convert.CcfEventsToJsonEvents(events)
   270  		if err != nil {
   271  			return nil, rpc.ConvertError(err, "failed to convert event payload", codes.Internal)
   272  		}
   273  	}
   274  
   275  	collectionID, err := t.lookupCollectionIDInBlock(block, txResult.TransactionID)
   276  	if err != nil {
   277  		return nil, err
   278  	}
   279  
   280  	return &access.TransactionResult{
   281  		TransactionID: txResult.TransactionID,
   282  		Status:        txStatus,
   283  		StatusCode:    txStatusCode,
   284  		Events:        events,
   285  		ErrorMessage:  txErrorMessage,
   286  		BlockID:       blockID,
   287  		BlockHeight:   block.Header.Height,
   288  		CollectionID:  collectionID,
   289  	}, nil
   290  }
   291  
   292  // deriveUnknownTransactionStatus is used to determine the status of transaction
   293  // that are not in a block yet based on the provided reference block ID.
   294  func (t *TransactionsLocalDataProvider) deriveUnknownTransactionStatus(refBlockID flow.Identifier) (flow.TransactionStatus, error) {
   295  	referenceBlock, err := t.state.AtBlockID(refBlockID).Head()
   296  	if err != nil {
   297  		return flow.TransactionStatusUnknown, err
   298  	}
   299  	refHeight := referenceBlock.Height
   300  	// get the latest finalized block from the state
   301  	finalized, err := t.state.Final().Head()
   302  	if err != nil {
   303  		return flow.TransactionStatusUnknown, irrecoverable.NewExceptionf("failed to lookup final header: %w", err)
   304  	}
   305  	finalizedHeight := finalized.Height
   306  
   307  	// if we haven't seen the expiry block for this transaction, it's not expired
   308  	if !isExpired(refHeight, finalizedHeight) {
   309  		return flow.TransactionStatusPending, nil
   310  	}
   311  
   312  	// At this point, we have seen the expiry block for the transaction.
   313  	// This means that, if no collections  prior to the expiry block contain
   314  	// the transaction, it can never be included and is expired.
   315  	//
   316  	// To ensure this, we need to have received all collections  up to the
   317  	// expiry block to ensure the transaction did not appear in any.
   318  
   319  	// the last full height is the height where we have received all
   320  	// collections  for all blocks with a lower height
   321  	fullHeight, err := t.blocks.GetLastFullBlockHeight()
   322  	if err != nil {
   323  		return flow.TransactionStatusUnknown, err
   324  	}
   325  
   326  	// if we have received collections  for all blocks up to the expiry block, the transaction is expired
   327  	if isExpired(refHeight, fullHeight) {
   328  		return flow.TransactionStatusExpired, nil
   329  	}
   330  
   331  	// tx found in transaction storage and collection storage but not in block storage
   332  	// However, this will not happen as of now since the ingestion engine doesn't subscribe
   333  	// for collections
   334  	return flow.TransactionStatusPending, nil
   335  }
   336  
   337  // deriveTransactionStatus is used to determine the status of a transaction based on the provided block ID, block height, and execution status.
   338  // No errors expected during normal operations.
   339  func (t *TransactionsLocalDataProvider) deriveTransactionStatus(blockID flow.Identifier, blockHeight uint64, executed bool) (flow.TransactionStatus, error) {
   340  	if !executed {
   341  		// If we've gotten here, but the block has not yet been executed, report it as only been finalized
   342  		return flow.TransactionStatusFinalized, nil
   343  	}
   344  
   345  	// From this point on, we know for sure this transaction has at least been executed
   346  
   347  	// get the latest sealed block from the State
   348  	sealed, err := t.state.Sealed().Head()
   349  	if err != nil {
   350  		return flow.TransactionStatusUnknown, irrecoverable.NewExceptionf("failed to lookup sealed header: %w", err)
   351  	}
   352  
   353  	if blockHeight > sealed.Height {
   354  		// The block is not yet sealed, so we'll report it as only executed
   355  		return flow.TransactionStatusExecuted, nil
   356  	}
   357  
   358  	// otherwise, this block has been executed, and sealed, so report as sealed
   359  	return flow.TransactionStatusSealed, nil
   360  }
   361  
   362  // isExpired checks whether a transaction is expired given the height of the
   363  // transaction's reference block and the height to compare against.
   364  func isExpired(refHeight, compareToHeight uint64) bool {
   365  	if compareToHeight <= refHeight {
   366  		return false
   367  	}
   368  	return compareToHeight-refHeight > flow.DefaultTransactionExpiry
   369  }
   370  
   371  // lookupCollectionIDInBlock returns the collection ID based on the transaction ID. The lookup is performed in block
   372  // collections.
   373  func (t *TransactionsLocalDataProvider) lookupCollectionIDInBlock(
   374  	block *flow.Block,
   375  	txID flow.Identifier,
   376  ) (flow.Identifier, error) {
   377  	for _, guarantee := range block.Payload.Guarantees {
   378  		collection, err := t.collections.LightByID(guarantee.ID())
   379  		if err != nil {
   380  			return flow.ZeroID, err
   381  		}
   382  
   383  		for _, collectionTxID := range collection.Transactions {
   384  			if collectionTxID == txID {
   385  				return guarantee.ID(), nil
   386  			}
   387  		}
   388  	}
   389  	return flow.ZeroID, status.Error(codes.NotFound, "transaction not found in block")
   390  }
   391  
   392  // buildTxIDToCollectionIDMapping returns a map of transaction ID to collection ID based on the provided block.
   393  // No errors expected during normal operations.
   394  func (t *TransactionsLocalDataProvider) buildTxIDToCollectionIDMapping(block *flow.Block) (map[flow.Identifier]flow.Identifier, error) {
   395  	txToCollectionID := make(map[flow.Identifier]flow.Identifier)
   396  	for _, guarantee := range block.Payload.Guarantees {
   397  		collection, err := t.collections.LightByID(guarantee.ID())
   398  		if err != nil {
   399  			// if the tx result is in storage, the collection must be too.
   400  			return nil, fmt.Errorf("failed to get collection %s in indexed block: %w", guarantee.ID(), err)
   401  		}
   402  		for _, txID := range collection.Transactions {
   403  			txToCollectionID[txID] = guarantee.ID()
   404  		}
   405  	}
   406  
   407  	txToCollectionID[t.systemTxID] = flow.ZeroID
   408  
   409  	return txToCollectionID, nil
   410  }