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 }