github.com/onflow/flow-go@v0.33.17/engine/access/rpc/backend/backend_transactions_test.go (about) 1 package backend 2 3 import ( 4 "context" 5 "fmt" 6 "math/rand" 7 8 "github.com/dgraph-io/badger/v2" 9 jsoncdc "github.com/onflow/cadence/encoding/json" 10 "github.com/onflow/flow/protobuf/go/flow/access" 11 "github.com/onflow/flow/protobuf/go/flow/entities" 12 execproto "github.com/onflow/flow/protobuf/go/flow/execution" 13 "github.com/stretchr/testify/mock" 14 "github.com/stretchr/testify/require" 15 "google.golang.org/grpc/codes" 16 "google.golang.org/grpc/status" 17 18 acc "github.com/onflow/flow-go/access" 19 connectionmock "github.com/onflow/flow-go/engine/access/rpc/connection/mock" 20 "github.com/onflow/flow-go/engine/common/rpc/convert" 21 "github.com/onflow/flow-go/fvm/blueprints" 22 "github.com/onflow/flow-go/model/flow" 23 "github.com/onflow/flow-go/model/flow/filter" 24 syncmock "github.com/onflow/flow-go/module/state_synchronization/mock" 25 "github.com/onflow/flow-go/state/protocol" 26 bprotocol "github.com/onflow/flow-go/state/protocol/badger" 27 "github.com/onflow/flow-go/state/protocol/util" 28 "github.com/onflow/flow-go/storage" 29 "github.com/onflow/flow-go/utils/unittest" 30 "github.com/onflow/flow-go/utils/unittest/generator" 31 ) 32 33 const expectedErrorMsg = "expected test error" 34 35 func (suite *Suite) withPreConfiguredState(f func(snap protocol.Snapshot)) { 36 identities := unittest.CompleteIdentitySet() 37 rootSnapshot := unittest.RootSnapshotFixture(identities) 38 util.RunWithFullProtocolState(suite.T(), rootSnapshot, func(db *badger.DB, state *bprotocol.ParticipantState) { 39 epochBuilder := unittest.NewEpochBuilder(suite.T(), state) 40 41 epochBuilder. 42 BuildEpoch(). 43 CompleteEpoch() 44 45 // get heights of each phase in built epochs 46 epoch1, ok := epochBuilder.EpochHeights(1) 47 require.True(suite.T(), ok) 48 49 // setup AtHeight mock returns for State 50 for _, height := range epoch1.Range() { 51 suite.state.On("AtHeight", epoch1.Range()).Return(state.AtHeight(height)) 52 } 53 54 snap := state.AtHeight(epoch1.FinalHeight()) 55 suite.state.On("Final").Return(snap) 56 suite.communicator.On("CallAvailableNode", 57 mock.Anything, 58 mock.Anything, 59 mock.Anything). 60 Return(nil).Once() 61 62 f(snap) 63 }) 64 } 65 66 // TestGetTransactionResultReturnsUnknown returns unknown result when tx not found 67 func (suite *Suite) TestGetTransactionResultReturnsUnknown() { 68 suite.withPreConfiguredState(func(snap protocol.Snapshot) { 69 block := unittest.BlockFixture() 70 tbody := unittest.TransactionBodyFixture() 71 tx := unittest.TransactionFixture() 72 tx.TransactionBody = tbody 73 74 coll := flow.CollectionFromTransactions([]*flow.Transaction{&tx}) 75 suite.state.On("AtBlockID", block.ID()).Return(snap, nil).Once() 76 77 suite.transactions. 78 On("ByID", tx.ID()). 79 Return(nil, storage.ErrNotFound) 80 81 params := suite.defaultBackendParams() 82 params.Communicator = suite.communicator 83 84 backend, err := New(params) 85 suite.Require().NoError(err) 86 87 res, err := backend.GetTransactionResult( 88 context.Background(), 89 tx.ID(), 90 block.ID(), 91 coll.ID(), 92 entities.EventEncodingVersion_JSON_CDC_V0, 93 ) 94 suite.Require().NoError(err) 95 suite.Require().Equal(res.Status, flow.TransactionStatusUnknown) 96 }) 97 } 98 99 // TestGetTransactionResultReturnsTransactionError returns error from transaction storage 100 func (suite *Suite) TestGetTransactionResultReturnsTransactionError() { 101 suite.withPreConfiguredState(func(snap protocol.Snapshot) { 102 block := unittest.BlockFixture() 103 tbody := unittest.TransactionBodyFixture() 104 tx := unittest.TransactionFixture() 105 tx.TransactionBody = tbody 106 107 coll := flow.CollectionFromTransactions([]*flow.Transaction{&tx}) 108 109 suite.transactions. 110 On("ByID", tx.ID()). 111 Return(nil, fmt.Errorf("some other error")) 112 113 suite.blocks. 114 On("ByID", block.ID()). 115 Return(&block, nil). 116 Once() 117 118 suite.state.On("AtBlockID", block.ID()).Return(snap, nil).Once() 119 120 params := suite.defaultBackendParams() 121 params.Communicator = suite.communicator 122 123 backend, err := New(params) 124 suite.Require().NoError(err) 125 126 _, err = backend.GetTransactionResult( 127 context.Background(), 128 tx.ID(), 129 block.ID(), 130 coll.ID(), 131 entities.EventEncodingVersion_JSON_CDC_V0, 132 ) 133 suite.Require().Equal(err, status.Errorf(codes.Internal, "failed to find: %v", fmt.Errorf("some other error"))) 134 }) 135 } 136 137 // TestGetTransactionResultReturnsValidTransactionResultFromHistoricNode tests lookup in historic nodes 138 func (suite *Suite) TestGetTransactionResultReturnsValidTransactionResultFromHistoricNode() { 139 suite.withPreConfiguredState(func(snap protocol.Snapshot) { 140 block := unittest.BlockFixture() 141 tbody := unittest.TransactionBodyFixture() 142 tx := unittest.TransactionFixture() 143 tx.TransactionBody = tbody 144 145 coll := flow.CollectionFromTransactions([]*flow.Transaction{&tx}) 146 147 suite.transactions. 148 On("ByID", tx.ID()). 149 Return(nil, storage.ErrNotFound) 150 151 suite.state.On("AtBlockID", block.ID()).Return(snap, nil).Once() 152 153 transactionResultResponse := access.TransactionResultResponse{ 154 Status: entities.TransactionStatus_EXECUTED, 155 StatusCode: uint32(entities.TransactionStatus_EXECUTED), 156 } 157 158 suite.historicalAccessClient. 159 On("GetTransactionResult", mock.Anything, mock.Anything). 160 Return(&transactionResultResponse, nil).Once() 161 162 params := suite.defaultBackendParams() 163 params.HistoricalAccessNodes = []access.AccessAPIClient{suite.historicalAccessClient} 164 params.Communicator = suite.communicator 165 166 backend, err := New(params) 167 suite.Require().NoError(err) 168 169 resp, err := backend.GetTransactionResult( 170 context.Background(), 171 tx.ID(), 172 block.ID(), 173 coll.ID(), 174 entities.EventEncodingVersion_JSON_CDC_V0, 175 ) 176 suite.Require().NoError(err) 177 suite.Require().Equal(flow.TransactionStatusExecuted, resp.Status) 178 suite.Require().Equal(uint(flow.TransactionStatusExecuted), resp.StatusCode) 179 }) 180 } 181 182 func (suite *Suite) withGetTransactionCachingTestSetup(f func(b *flow.Block, t *flow.Transaction)) { 183 suite.withPreConfiguredState(func(snap protocol.Snapshot) { 184 block := unittest.BlockFixture() 185 tbody := unittest.TransactionBodyFixture() 186 tx := unittest.TransactionFixture() 187 tx.TransactionBody = tbody 188 189 suite.transactions. 190 On("ByID", tx.ID()). 191 Return(nil, storage.ErrNotFound) 192 193 suite.state.On("AtBlockID", block.ID()).Return(snap, nil).Once() 194 195 f(&block, &tx) 196 }) 197 } 198 199 // TestGetTransactionResultFromCache get historic transaction result from cache 200 func (suite *Suite) TestGetTransactionResultFromCache() { 201 suite.withGetTransactionCachingTestSetup(func(block *flow.Block, tx *flow.Transaction) { 202 transactionResultResponse := access.TransactionResultResponse{ 203 Status: entities.TransactionStatus_EXECUTED, 204 StatusCode: uint32(entities.TransactionStatus_EXECUTED), 205 } 206 207 suite.historicalAccessClient. 208 On("GetTransactionResult", mock.AnythingOfType("*context.emptyCtx"), mock.AnythingOfType("*access.GetTransactionRequest")). 209 Return(&transactionResultResponse, nil).Once() 210 211 params := suite.defaultBackendParams() 212 params.HistoricalAccessNodes = []access.AccessAPIClient{suite.historicalAccessClient} 213 params.Communicator = suite.communicator 214 params.TxResultCacheSize = 10 215 216 backend, err := New(params) 217 suite.Require().NoError(err) 218 219 coll := flow.CollectionFromTransactions([]*flow.Transaction{tx}) 220 221 resp, err := backend.GetTransactionResult( 222 context.Background(), 223 tx.ID(), 224 block.ID(), 225 coll.ID(), 226 entities.EventEncodingVersion_JSON_CDC_V0, 227 ) 228 suite.Require().NoError(err) 229 suite.Require().Equal(flow.TransactionStatusExecuted, resp.Status) 230 suite.Require().Equal(uint(flow.TransactionStatusExecuted), resp.StatusCode) 231 232 resp2, err := backend.GetTransactionResult( 233 context.Background(), 234 tx.ID(), 235 block.ID(), 236 coll.ID(), 237 entities.EventEncodingVersion_JSON_CDC_V0, 238 ) 239 suite.Require().NoError(err) 240 suite.Require().Equal(flow.TransactionStatusExecuted, resp2.Status) 241 suite.Require().Equal(uint(flow.TransactionStatusExecuted), resp2.StatusCode) 242 243 suite.historicalAccessClient.AssertExpectations(suite.T()) 244 }) 245 } 246 247 // TestGetTransactionResultCacheNonExistent tests caches non existing result 248 func (suite *Suite) TestGetTransactionResultCacheNonExistent() { 249 suite.withGetTransactionCachingTestSetup(func(block *flow.Block, tx *flow.Transaction) { 250 suite.historicalAccessClient. 251 On("GetTransactionResult", mock.AnythingOfType("*context.emptyCtx"), mock.AnythingOfType("*access.GetTransactionRequest")). 252 Return(nil, status.Errorf(codes.NotFound, "no known transaction with ID %s", tx.ID())).Once() 253 254 params := suite.defaultBackendParams() 255 params.HistoricalAccessNodes = []access.AccessAPIClient{suite.historicalAccessClient} 256 params.Communicator = suite.communicator 257 params.TxResultCacheSize = 10 258 259 backend, err := New(params) 260 suite.Require().NoError(err) 261 262 coll := flow.CollectionFromTransactions([]*flow.Transaction{tx}) 263 264 resp, err := backend.GetTransactionResult( 265 context.Background(), 266 tx.ID(), 267 block.ID(), 268 coll.ID(), 269 entities.EventEncodingVersion_JSON_CDC_V0, 270 ) 271 suite.Require().NoError(err) 272 suite.Require().Equal(flow.TransactionStatusUnknown, resp.Status) 273 suite.Require().Equal(uint(flow.TransactionStatusUnknown), resp.StatusCode) 274 275 // ensure the unknown transaction is cached when not found anywhere 276 txStatus := flow.TransactionStatusUnknown 277 res, ok := backend.txResultCache.Get(tx.ID()) 278 suite.Require().True(ok) 279 suite.Require().Equal(res, &acc.TransactionResult{ 280 Status: txStatus, 281 StatusCode: uint(txStatus), 282 }) 283 284 suite.historicalAccessClient.AssertExpectations(suite.T()) 285 }) 286 } 287 288 // TestGetTransactionResultUnknownFromCache retrieve unknown result from cache. 289 func (suite *Suite) TestGetTransactionResultUnknownFromCache() { 290 suite.withGetTransactionCachingTestSetup(func(block *flow.Block, tx *flow.Transaction) { 291 suite.historicalAccessClient. 292 On("GetTransactionResult", mock.AnythingOfType("*context.emptyCtx"), mock.AnythingOfType("*access.GetTransactionRequest")). 293 Return(nil, status.Errorf(codes.NotFound, "no known transaction with ID %s", tx.ID())).Once() 294 295 params := suite.defaultBackendParams() 296 params.HistoricalAccessNodes = []access.AccessAPIClient{suite.historicalAccessClient} 297 params.Communicator = suite.communicator 298 params.TxResultCacheSize = 10 299 300 backend, err := New(params) 301 suite.Require().NoError(err) 302 303 coll := flow.CollectionFromTransactions([]*flow.Transaction{tx}) 304 305 resp, err := backend.GetTransactionResult( 306 context.Background(), 307 tx.ID(), 308 block.ID(), 309 coll.ID(), 310 entities.EventEncodingVersion_JSON_CDC_V0, 311 ) 312 suite.Require().NoError(err) 313 suite.Require().Equal(flow.TransactionStatusUnknown, resp.Status) 314 suite.Require().Equal(uint(flow.TransactionStatusUnknown), resp.StatusCode) 315 316 // ensure the unknown transaction is cached when not found anywhere 317 txStatus := flow.TransactionStatusUnknown 318 res, ok := backend.txResultCache.Get(tx.ID()) 319 suite.Require().True(ok) 320 suite.Require().Equal(res, &acc.TransactionResult{ 321 Status: txStatus, 322 StatusCode: uint(txStatus), 323 }) 324 325 resp2, err := backend.GetTransactionResult( 326 context.Background(), 327 tx.ID(), 328 block.ID(), 329 coll.ID(), 330 entities.EventEncodingVersion_JSON_CDC_V0, 331 ) 332 suite.Require().NoError(err) 333 suite.Require().Equal(flow.TransactionStatusUnknown, resp2.Status) 334 suite.Require().Equal(uint(flow.TransactionStatusUnknown), resp2.StatusCode) 335 336 suite.historicalAccessClient.AssertExpectations(suite.T()) 337 }) 338 } 339 340 // TestLookupTransactionErrorMessage_HappyPath tests lookup of a transaction error message. In a happy path if it wasn't found in the cache, it 341 // has to be fetched from the execution node, otherwise served from the cache. 342 // If the transaction has not failed, the error message must be empty. 343 func (suite *Suite) TestLookupTransactionErrorMessage_HappyPath() { 344 block := unittest.BlockFixture() 345 blockId := block.ID() 346 failedTx := unittest.TransactionFixture() 347 failedTxId := failedTx.ID() 348 349 _, fixedENIDs := suite.setupReceipts(&block) 350 suite.state.On("Final").Return(suite.snapshot, nil).Maybe() 351 suite.snapshot.On("Identities", mock.Anything).Return(fixedENIDs, nil) 352 353 // create a mock connection factory 354 connFactory := connectionmock.NewConnectionFactory(suite.T()) 355 connFactory.On("GetExecutionAPIClient", mock.Anything).Return(suite.execClient, &mockCloser{}, nil) 356 357 params := suite.defaultBackendParams() 358 // the connection factory should be used to get the execution node client 359 params.ConnFactory = connFactory 360 params.FixedExecutionNodeIDs = fixedENIDs.NodeIDs().Strings() 361 362 backend, err := New(params) 363 suite.Require().NoError(err) 364 365 expectedErrorMsg := "some error" 366 367 exeEventReq := &execproto.GetTransactionErrorMessageRequest{ 368 BlockId: blockId[:], 369 TransactionId: failedTxId[:], 370 } 371 372 exeEventResp := &execproto.GetTransactionErrorMessageResponse{ 373 TransactionId: failedTxId[:], 374 ErrorMessage: expectedErrorMsg, 375 } 376 377 suite.execClient.On("GetTransactionErrorMessage", mock.Anything, exeEventReq).Return(exeEventResp, nil).Once() 378 379 errMsg, err := backend.LookupErrorMessageByTransactionID(context.Background(), blockId, failedTxId) 380 suite.Require().NoError(err) 381 suite.Require().Equal(expectedErrorMsg, errMsg) 382 383 // ensure the transaction error message is cached after retrieval; we do this by mocking the grpc call 384 // only once 385 errMsg, err = backend.LookupErrorMessageByTransactionID(context.Background(), blockId, failedTxId) 386 suite.Require().NoError(err) 387 suite.Require().Equal(expectedErrorMsg, errMsg) 388 suite.assertAllExpectations() 389 } 390 391 // TestLookupTransactionErrorMessage_FailedToFetch tests lookup of a transaction error message, when a transaction result 392 // is not in the cache and needs to be fetched from EN, but the EN fails to return it. 393 func (suite *Suite) TestLookupTransactionErrorMessage_FailedToFetch() { 394 block := unittest.BlockFixture() 395 blockId := block.ID() 396 failedTx := unittest.TransactionFixture() 397 failedTxId := failedTx.ID() 398 399 _, fixedENIDs := suite.setupReceipts(&block) 400 suite.state.On("Final").Return(suite.snapshot, nil).Maybe() 401 suite.snapshot.On("Identities", mock.Anything).Return(fixedENIDs, nil) 402 403 // create a mock connection factory 404 connFactory := connectionmock.NewConnectionFactory(suite.T()) 405 connFactory.On("GetExecutionAPIClient", mock.Anything).Return(suite.execClient, &mockCloser{}, nil) 406 407 params := suite.defaultBackendParams() 408 // the connection factory should be used to get the execution node client 409 params.ConnFactory = connFactory 410 params.FixedExecutionNodeIDs = fixedENIDs.NodeIDs().Strings() 411 412 backend, err := New(params) 413 suite.Require().NoError(err) 414 415 // lookup should try each of the 2 ENs in fixedENIDs 416 suite.execClient.On("GetTransactionErrorMessage", mock.Anything, mock.Anything).Return(nil, 417 status.Error(codes.Unavailable, "")).Twice() 418 419 errMsg, err := backend.LookupErrorMessageByTransactionID(context.Background(), blockId, failedTxId) 420 suite.Require().Error(err) 421 suite.Require().Equal(codes.Unavailable, status.Code(err)) 422 suite.Require().Empty(errMsg) 423 424 suite.assertAllExpectations() 425 } 426 427 // TestLookupTransactionErrorMessageByIndex_HappyPath tests lookup of a transaction error message by index. 428 // In a happy path if it wasn't found in the cache, it has to be fetched from the execution node, otherwise served from the cache. 429 // If the transaction has not failed, the error message must be empty. 430 func (suite *Suite) TestLookupTransactionErrorMessageByIndex_HappyPath() { 431 block := unittest.BlockFixture() 432 blockId := block.ID() 433 failedTx := unittest.TransactionFixture() 434 failedTxId := failedTx.ID() 435 failedTxIndex := rand.Uint32() 436 437 suite.transactionResults.On("ByBlockIDTransactionIndex", blockId, failedTxIndex). 438 Return(&flow.LightTransactionResult{ 439 TransactionID: failedTxId, 440 Failed: true, 441 ComputationUsed: 0, 442 }, nil).Twice() 443 444 _, fixedENIDs := suite.setupReceipts(&block) 445 suite.state.On("Final").Return(suite.snapshot, nil).Maybe() 446 suite.snapshot.On("Identities", mock.Anything).Return(fixedENIDs, nil) 447 448 // create a mock connection factory 449 connFactory := connectionmock.NewConnectionFactory(suite.T()) 450 connFactory.On("GetExecutionAPIClient", mock.Anything).Return(suite.execClient, &mockCloser{}, nil) 451 452 // create a mock index reporter 453 reporter := syncmock.NewIndexReporter(suite.T()) 454 reporter.On("LowestIndexedHeight").Return(block.Header.Height, nil) 455 reporter.On("HighestIndexedHeight").Return(block.Header.Height+10, nil) 456 457 params := suite.defaultBackendParams() 458 459 // the connection factory should be used to get the execution node client 460 params.ConnFactory = connFactory 461 params.FixedExecutionNodeIDs = fixedENIDs.NodeIDs().Strings() 462 463 params.TxResultsIndex = NewTransactionResultsIndex(suite.transactionResults) 464 err := params.TxResultsIndex.Initialize(reporter) 465 suite.Require().NoError(err) 466 467 backend, err := New(params) 468 suite.Require().NoError(err) 469 470 expectedErrorMsg := "some error" 471 472 exeEventReq := &execproto.GetTransactionErrorMessageByIndexRequest{ 473 BlockId: blockId[:], 474 Index: failedTxIndex, 475 } 476 477 exeEventResp := &execproto.GetTransactionErrorMessageResponse{ 478 TransactionId: failedTxId[:], 479 ErrorMessage: expectedErrorMsg, 480 } 481 482 suite.execClient.On("GetTransactionErrorMessageByIndex", mock.Anything, exeEventReq).Return(exeEventResp, nil).Once() 483 484 errMsg, err := backend.LookupErrorMessageByIndex(context.Background(), blockId, block.Header.Height, failedTxIndex) 485 suite.Require().NoError(err) 486 suite.Require().Equal(expectedErrorMsg, errMsg) 487 488 // ensure the transaction error message is cached after retrieval; we do this by mocking the grpc call 489 // only once 490 errMsg, err = backend.LookupErrorMessageByIndex(context.Background(), blockId, block.Header.Height, failedTxIndex) 491 suite.Require().NoError(err) 492 suite.Require().Equal(expectedErrorMsg, errMsg) 493 suite.assertAllExpectations() 494 } 495 496 // TestLookupTransactionErrorMessageByIndex_UnknownTransaction tests lookup of a transaction error message by index, 497 // when a transaction result has not been synced yet, in this case nothing we can do but return an error. 498 func (suite *Suite) TestLookupTransactionErrorMessageByIndex_UnknownTransaction() { 499 block := unittest.BlockFixture() 500 blockId := block.ID() 501 failedTxIndex := rand.Uint32() 502 503 suite.transactionResults.On("ByBlockIDTransactionIndex", blockId, failedTxIndex). 504 Return(nil, storage.ErrNotFound).Once() 505 506 // create a mock index reporter 507 reporter := syncmock.NewIndexReporter(suite.T()) 508 reporter.On("LowestIndexedHeight").Return(block.Header.Height, nil) 509 reporter.On("HighestIndexedHeight").Return(block.Header.Height+10, nil) 510 511 params := suite.defaultBackendParams() 512 513 params.TxResultsIndex = NewTransactionResultsIndex(suite.transactionResults) 514 err := params.TxResultsIndex.Initialize(reporter) 515 suite.Require().NoError(err) 516 517 backend, err := New(params) 518 suite.Require().NoError(err) 519 520 errMsg, err := backend.LookupErrorMessageByIndex(context.Background(), blockId, block.Header.Height, failedTxIndex) 521 suite.Require().Error(err) 522 suite.Require().Equal(codes.NotFound, status.Code(err)) 523 suite.Require().Empty(errMsg) 524 525 suite.assertAllExpectations() 526 } 527 528 // TestLookupTransactionErrorMessageByIndex_FailedToFetch tests lookup of a transaction error message by index, 529 // when a transaction result is not in the cache and needs to be fetched from EN, but the EN fails to return it. 530 func (suite *Suite) TestLookupTransactionErrorMessageByIndex_FailedToFetch() { 531 block := unittest.BlockFixture() 532 blockId := block.ID() 533 failedTx := unittest.TransactionFixture() 534 failedTxId := failedTx.ID() 535 failedTxIndex := rand.Uint32() 536 537 suite.transactionResults.On("ByBlockIDTransactionIndex", blockId, failedTxIndex). 538 Return(&flow.LightTransactionResult{ 539 TransactionID: failedTxId, 540 Failed: true, 541 ComputationUsed: 0, 542 }, nil).Once() 543 544 _, fixedENIDs := suite.setupReceipts(&block) 545 suite.state.On("Final").Return(suite.snapshot, nil).Maybe() 546 suite.snapshot.On("Identities", mock.Anything).Return(fixedENIDs, nil) 547 548 // create a mock connection factory 549 connFactory := connectionmock.NewConnectionFactory(suite.T()) 550 connFactory.On("GetExecutionAPIClient", mock.Anything).Return(suite.execClient, &mockCloser{}, nil) 551 552 // create a mock index reporter 553 reporter := syncmock.NewIndexReporter(suite.T()) 554 reporter.On("LowestIndexedHeight").Return(block.Header.Height, nil) 555 reporter.On("HighestIndexedHeight").Return(block.Header.Height+10, nil) 556 557 params := suite.defaultBackendParams() 558 // the connection factory should be used to get the execution node client 559 params.ConnFactory = connFactory 560 params.FixedExecutionNodeIDs = fixedENIDs.NodeIDs().Strings() 561 562 params.TxResultsIndex = NewTransactionResultsIndex(suite.transactionResults) 563 err := params.TxResultsIndex.Initialize(reporter) 564 suite.Require().NoError(err) 565 566 backend, err := New(params) 567 suite.Require().NoError(err) 568 569 // lookup should try each of the 2 ENs in fixedENIDs 570 suite.execClient.On("GetTransactionErrorMessageByIndex", mock.Anything, mock.Anything).Return(nil, 571 status.Error(codes.Unavailable, "")).Twice() 572 573 errMsg, err := backend.LookupErrorMessageByIndex(context.Background(), blockId, block.Header.Height, failedTxIndex) 574 suite.Require().Error(err) 575 suite.Require().Equal(codes.Unavailable, status.Code(err)) 576 suite.Require().Empty(errMsg) 577 578 suite.assertAllExpectations() 579 } 580 581 // TestLookupTransactionErrorMessages_HappyPath tests lookup of a transaction error messages by block ID. 582 // In a happy path, it has to be fetched from the execution node if there are no cached results. 583 // All fetched transactions have to be cached for future calls. 584 func (suite *Suite) TestLookupTransactionErrorMessages_HappyPath() { 585 block := unittest.BlockFixture() 586 blockId := block.ID() 587 588 resultsByBlockID := make([]flow.LightTransactionResult, 0) 589 for i := 0; i < 5; i++ { 590 resultsByBlockID = append(resultsByBlockID, flow.LightTransactionResult{ 591 TransactionID: unittest.IdentifierFixture(), 592 Failed: i%2 == 0, // create a mix of failed and non-failed transactions 593 ComputationUsed: 0, 594 }) 595 } 596 597 suite.transactionResults.On("ByBlockID", blockId). 598 Return(resultsByBlockID, nil).Twice() 599 600 _, fixedENIDs := suite.setupReceipts(&block) 601 suite.state.On("Final").Return(suite.snapshot, nil).Maybe() 602 suite.snapshot.On("Identities", mock.Anything).Return(fixedENIDs, nil) 603 604 // create a mock connection factory 605 connFactory := connectionmock.NewConnectionFactory(suite.T()) 606 connFactory.On("GetExecutionAPIClient", mock.Anything).Return(suite.execClient, &mockCloser{}, nil) 607 608 // create a mock index reporter 609 reporter := syncmock.NewIndexReporter(suite.T()) 610 reporter.On("LowestIndexedHeight").Return(block.Header.Height, nil) 611 reporter.On("HighestIndexedHeight").Return(block.Header.Height+10, nil) 612 613 params := suite.defaultBackendParams() 614 615 // the connection factory should be used to get the execution node client 616 params.ConnFactory = connFactory 617 params.FixedExecutionNodeIDs = fixedENIDs.NodeIDs().Strings() 618 619 params.TxResultsIndex = NewTransactionResultsIndex(suite.transactionResults) 620 err := params.TxResultsIndex.Initialize(reporter) 621 suite.Require().NoError(err) 622 623 backend, err := New(params) 624 suite.Require().NoError(err) 625 626 expectedErrorMsg := "some error" 627 628 exeEventReq := &execproto.GetTransactionErrorMessagesByBlockIDRequest{ 629 BlockId: blockId[:], 630 } 631 632 exeEventResp := &execproto.GetTransactionErrorMessagesResponse{} 633 for _, result := range resultsByBlockID { 634 r := result 635 if r.Failed { 636 errMsg := fmt.Sprintf("%s.%s", expectedErrorMsg, r.TransactionID) 637 exeEventResp.Results = append(exeEventResp.Results, &execproto.GetTransactionErrorMessagesResponse_Result{ 638 TransactionId: r.TransactionID[:], 639 ErrorMessage: errMsg, 640 }) 641 } 642 } 643 644 suite.execClient.On("GetTransactionErrorMessagesByBlockID", mock.Anything, exeEventReq). 645 Return(exeEventResp, nil). 646 Once() 647 648 errMessages, err := backend.LookupErrorMessagesByBlockID(context.Background(), blockId, block.Header.Height) 649 suite.Require().NoError(err) 650 suite.Require().Len(errMessages, len(exeEventResp.Results)) 651 for _, expectedResult := range exeEventResp.Results { 652 errMsg, ok := errMessages[convert.MessageToIdentifier(expectedResult.TransactionId)] 653 suite.Require().True(ok) 654 suite.Assert().Equal(expectedResult.ErrorMessage, errMsg) 655 } 656 657 // ensure the transaction error message is cached after retrieval; we do this by mocking the grpc call 658 // only once 659 errMessages, err = backend.LookupErrorMessagesByBlockID(context.Background(), blockId, block.Header.Height) 660 suite.Require().NoError(err) 661 suite.Require().Len(errMessages, len(exeEventResp.Results)) 662 for _, expectedResult := range exeEventResp.Results { 663 errMsg, ok := errMessages[convert.MessageToIdentifier(expectedResult.TransactionId)] 664 suite.Require().True(ok) 665 suite.Assert().Equal(expectedResult.ErrorMessage, errMsg) 666 } 667 suite.assertAllExpectations() 668 } 669 670 // TestLookupTransactionErrorMessages_HappyPath_NoFailedTxns tests lookup of a transaction error messages by block ID. 671 // In a happy path where a block with no failed txns is requested. We don't want to perform an RPC call in this case. 672 func (suite *Suite) TestLookupTransactionErrorMessages_HappyPath_NoFailedTxns() { 673 block := unittest.BlockFixture() 674 blockId := block.ID() 675 676 resultsByBlockID := []flow.LightTransactionResult{ 677 { 678 TransactionID: unittest.IdentifierFixture(), 679 Failed: false, 680 ComputationUsed: 0, 681 }, 682 { 683 TransactionID: unittest.IdentifierFixture(), 684 Failed: false, 685 ComputationUsed: 0, 686 }, 687 } 688 689 suite.transactionResults.On("ByBlockID", blockId). 690 Return(resultsByBlockID, nil).Once() 691 692 // create a mock index reporter 693 reporter := syncmock.NewIndexReporter(suite.T()) 694 reporter.On("LowestIndexedHeight").Return(block.Header.Height, nil) 695 reporter.On("HighestIndexedHeight").Return(block.Header.Height+10, nil) 696 697 params := suite.defaultBackendParams() 698 699 params.TxResultsIndex = NewTransactionResultsIndex(suite.transactionResults) 700 err := params.TxResultsIndex.Initialize(reporter) 701 suite.Require().NoError(err) 702 703 backend, err := New(params) 704 suite.Require().NoError(err) 705 706 errMessages, err := backend.LookupErrorMessagesByBlockID(context.Background(), blockId, block.Header.Height) 707 suite.Require().NoError(err) 708 suite.Require().Empty(errMessages) 709 suite.assertAllExpectations() 710 } 711 712 // TestLookupTransactionErrorMessages_UnknownTransaction tests lookup of a transaction error messages by block ID, 713 // when a transaction results for block has not been synced yet, in this case nothing we can do but return an error. 714 func (suite *Suite) TestLookupTransactionErrorMessages_UnknownTransaction() { 715 block := unittest.BlockFixture() 716 blockId := block.ID() 717 718 suite.transactionResults.On("ByBlockID", blockId). 719 Return(nil, storage.ErrNotFound).Once() 720 721 // create a mock index reporter 722 reporter := syncmock.NewIndexReporter(suite.T()) 723 reporter.On("LowestIndexedHeight").Return(block.Header.Height, nil) 724 reporter.On("HighestIndexedHeight").Return(block.Header.Height+10, nil) 725 726 params := suite.defaultBackendParams() 727 728 params.TxResultsIndex = NewTransactionResultsIndex(suite.transactionResults) 729 err := params.TxResultsIndex.Initialize(reporter) 730 suite.Require().NoError(err) 731 732 backend, err := New(params) 733 suite.Require().NoError(err) 734 735 errMsg, err := backend.LookupErrorMessagesByBlockID(context.Background(), blockId, block.Header.Height) 736 suite.Require().Error(err) 737 suite.Require().Equal(codes.NotFound, status.Code(err)) 738 suite.Require().Empty(errMsg) 739 740 suite.assertAllExpectations() 741 } 742 743 // TestLookupTransactionErrorMessages_FailedToFetch tests lookup of a transaction error messages by block ID, 744 // when a transaction result is not in the cache and needs to be fetched from EN, but the EN fails to return it. 745 func (suite *Suite) TestLookupTransactionErrorMessages_FailedToFetch() { 746 block := unittest.BlockFixture() 747 blockId := block.ID() 748 749 resultsByBlockID := []flow.LightTransactionResult{ 750 { 751 TransactionID: unittest.IdentifierFixture(), 752 Failed: true, 753 ComputationUsed: 0, 754 }, 755 { 756 TransactionID: unittest.IdentifierFixture(), 757 Failed: true, 758 ComputationUsed: 0, 759 }, 760 } 761 762 suite.transactionResults.On("ByBlockID", blockId). 763 Return(resultsByBlockID, nil).Once() 764 765 _, fixedENIDs := suite.setupReceipts(&block) 766 suite.state.On("Final").Return(suite.snapshot, nil).Maybe() 767 suite.snapshot.On("Identities", mock.Anything).Return(fixedENIDs, nil) 768 769 // create a mock connection factory 770 connFactory := connectionmock.NewConnectionFactory(suite.T()) 771 connFactory.On("GetExecutionAPIClient", mock.Anything).Return(suite.execClient, &mockCloser{}, nil) 772 773 // create a mock index reporter 774 reporter := syncmock.NewIndexReporter(suite.T()) 775 reporter.On("LowestIndexedHeight").Return(block.Header.Height, nil) 776 reporter.On("HighestIndexedHeight").Return(block.Header.Height+10, nil) 777 778 params := suite.defaultBackendParams() 779 // the connection factory should be used to get the execution node client 780 params.ConnFactory = connFactory 781 params.FixedExecutionNodeIDs = fixedENIDs.NodeIDs().Strings() 782 783 params.TxResultsIndex = NewTransactionResultsIndex(suite.transactionResults) 784 err := params.TxResultsIndex.Initialize(reporter) 785 suite.Require().NoError(err) 786 787 backend, err := New(params) 788 suite.Require().NoError(err) 789 790 // pretend the first transaction has been cached, but there are multiple failed txns so still a request has to be made. 791 backend.txErrorMessagesCache.Add(resultsByBlockID[0].TransactionID, "some error") 792 793 suite.execClient.On("GetTransactionErrorMessagesByBlockID", mock.Anything, mock.Anything).Return(nil, 794 status.Error(codes.Unavailable, "")).Twice() 795 796 errMsg, err := backend.LookupErrorMessagesByBlockID(context.Background(), blockId, block.Header.Height) 797 suite.Require().Error(err) 798 suite.Require().Equal(codes.Unavailable, status.Code(err)) 799 suite.Require().Empty(errMsg) 800 801 suite.assertAllExpectations() 802 } 803 804 // TestGetSystemTransaction_HappyPath tests that GetSystemTransaction call returns system chunk transaction. 805 func (suite *Suite) TestGetSystemTransaction_HappyPath() { 806 suite.withPreConfiguredState(func(snap protocol.Snapshot) { 807 suite.state.On("Sealed").Return(snap, nil).Maybe() 808 809 params := suite.defaultBackendParams() 810 backend, err := New(params) 811 suite.Require().NoError(err) 812 813 block := unittest.BlockFixture() 814 blockID := block.ID() 815 816 // Make the call for the system chunk transaction 817 res, err := backend.GetSystemTransaction(context.Background(), blockID) 818 suite.Require().NoError(err) 819 // Expected system chunk transaction 820 systemTx, err := blueprints.SystemChunkTransaction(suite.chainID.Chain()) 821 suite.Require().NoError(err) 822 823 suite.Require().Equal(systemTx, res) 824 }) 825 } 826 827 // TestGetSystemTransactionResult_HappyPath tests that GetSystemTransactionResult call returns system transaction 828 // result for required block id. 829 func (suite *Suite) TestGetSystemTransactionResult_HappyPath() { 830 suite.withPreConfiguredState(func(snap protocol.Snapshot) { 831 suite.state.On("Sealed").Return(snap, nil).Maybe() 832 lastBlock, err := snap.Head() 833 suite.Require().NoError(err) 834 identities, err := snap.Identities(filter.Any) 835 suite.Require().NoError(err) 836 837 block := unittest.BlockWithParentFixture(lastBlock) 838 blockID := block.ID() 839 suite.state.On("AtBlockID", blockID).Return( 840 unittest.StateSnapshotForKnownBlock(block.Header, identities.Lookup()), nil).Once() 841 842 // block storage returns the corresponding block 843 suite.blocks. 844 On("ByID", blockID). 845 Return(block, nil). 846 Once() 847 848 receipt1 := unittest.ReceiptForBlockFixture(block) 849 suite.receipts. 850 On("ByBlockID", block.ID()). 851 Return(flow.ExecutionReceiptList{receipt1}, nil) 852 853 // create a mock connection factory 854 connFactory := connectionmock.NewConnectionFactory(suite.T()) 855 connFactory.On("GetExecutionAPIClient", mock.Anything).Return(suite.execClient, &mockCloser{}, nil) 856 857 // the connection factory should be used to get the execution node client 858 params := suite.defaultBackendParams() 859 params.ConnFactory = connFactory 860 861 exeEventReq := &execproto.GetTransactionsByBlockIDRequest{ 862 BlockId: blockID[:], 863 } 864 865 // Generating events with event generator 866 exeNodeEventEncodingVersion := entities.EventEncodingVersion_CCF_V0 867 events := generator.GetEventsWithEncoding(1, exeNodeEventEncodingVersion) 868 eventMessages := convert.EventsToMessages(events) 869 870 exeEventResp := &execproto.GetTransactionResultsResponse{ 871 TransactionResults: []*execproto.GetTransactionResultResponse{{ 872 Events: eventMessages, 873 EventEncodingVersion: exeNodeEventEncodingVersion, 874 }}, 875 EventEncodingVersion: exeNodeEventEncodingVersion, 876 } 877 878 suite.execClient. 879 On("GetTransactionResultsByBlockID", mock.Anything, exeEventReq). 880 Return(exeEventResp, nil). 881 Once() 882 883 backend, err := New(params) 884 suite.Require().NoError(err) 885 886 // Make the call for the system transaction result 887 res, err := backend.GetSystemTransactionResult( 888 context.Background(), 889 block.ID(), 890 entities.EventEncodingVersion_JSON_CDC_V0, 891 ) 892 suite.Require().NoError(err) 893 894 // Expected system chunk transaction 895 systemTx, err := blueprints.SystemChunkTransaction(suite.chainID.Chain()) 896 suite.Require().NoError(err) 897 898 suite.Require().Equal(flow.TransactionStatusExecuted, res.Status) 899 suite.Require().Equal(systemTx.ID(), res.TransactionID) 900 901 // Check for successful decoding of event 902 _, err = jsoncdc.Decode(nil, res.Events[0].Payload) 903 suite.Require().NoError(err) 904 905 events, err = convert.MessagesToEventsWithEncodingConversion(eventMessages, 906 exeNodeEventEncodingVersion, 907 entities.EventEncodingVersion_JSON_CDC_V0) 908 suite.Require().NoError(err) 909 suite.Require().Equal(events, res.Events) 910 }) 911 } 912 913 // TestGetSystemTransactionResult_BlockNotFound tests GetSystemTransactionResult function when block was not found. 914 func (suite *Suite) TestGetSystemTransactionResult_BlockNotFound() { 915 suite.withPreConfiguredState(func(snap protocol.Snapshot) { 916 suite.state.On("Sealed").Return(snap, nil).Maybe() 917 lastBlock, err := snap.Head() 918 suite.Require().NoError(err) 919 identities, err := snap.Identities(filter.Any) 920 suite.Require().NoError(err) 921 922 block := unittest.BlockWithParentFixture(lastBlock) 923 blockID := block.ID() 924 suite.state.On("AtBlockID", blockID).Return( 925 unittest.StateSnapshotForKnownBlock(block.Header, identities.Lookup()), nil).Once() 926 927 // block storage returns the ErrNotFound error 928 suite.blocks. 929 On("ByID", blockID). 930 Return(nil, storage.ErrNotFound). 931 Once() 932 933 receipt1 := unittest.ReceiptForBlockFixture(block) 934 suite.receipts. 935 On("ByBlockID", block.ID()). 936 Return(flow.ExecutionReceiptList{receipt1}, nil) 937 938 params := suite.defaultBackendParams() 939 940 backend, err := New(params) 941 suite.Require().NoError(err) 942 943 // Make the call for the system transaction result 944 res, err := backend.GetSystemTransactionResult( 945 context.Background(), 946 block.ID(), 947 entities.EventEncodingVersion_JSON_CDC_V0, 948 ) 949 950 suite.Require().Nil(res) 951 suite.Require().Error(err) 952 suite.Require().Equal(err, status.Errorf(codes.NotFound, "not found: %v", fmt.Errorf("key not found"))) 953 }) 954 } 955 956 // TestGetSystemTransactionResult_FailedEncodingConversion tests the GetSystemTransactionResult function with different 957 // event encoding versions. 958 func (suite *Suite) TestGetSystemTransactionResult_FailedEncodingConversion() { 959 suite.withPreConfiguredState(func(snap protocol.Snapshot) { 960 suite.state.On("Sealed").Return(snap, nil).Maybe() 961 lastBlock, err := snap.Head() 962 suite.Require().NoError(err) 963 identities, err := snap.Identities(filter.Any) 964 suite.Require().NoError(err) 965 966 block := unittest.BlockWithParentFixture(lastBlock) 967 blockID := block.ID() 968 suite.state.On("AtBlockID", blockID).Return( 969 unittest.StateSnapshotForKnownBlock(block.Header, identities.Lookup()), nil).Once() 970 971 // block storage returns the corresponding block 972 suite.blocks. 973 On("ByID", blockID). 974 Return(block, nil). 975 Once() 976 977 receipt1 := unittest.ReceiptForBlockFixture(block) 978 suite.receipts. 979 On("ByBlockID", block.ID()). 980 Return(flow.ExecutionReceiptList{receipt1}, nil) 981 982 // create a mock connection factory 983 connFactory := connectionmock.NewConnectionFactory(suite.T()) 984 connFactory.On("GetExecutionAPIClient", mock.Anything).Return(suite.execClient, &mockCloser{}, nil) 985 986 // the connection factory should be used to get the execution node client 987 params := suite.defaultBackendParams() 988 params.ConnFactory = connFactory 989 990 exeEventReq := &execproto.GetTransactionsByBlockIDRequest{ 991 BlockId: blockID[:], 992 } 993 994 // create empty events 995 eventsPerBlock := 10 996 eventMessages := make([]*entities.Event, eventsPerBlock) 997 998 exeEventResp := &execproto.GetTransactionResultsResponse{ 999 TransactionResults: []*execproto.GetTransactionResultResponse{{ 1000 Events: eventMessages, 1001 EventEncodingVersion: entities.EventEncodingVersion_JSON_CDC_V0, 1002 }}, 1003 } 1004 1005 suite.execClient. 1006 On("GetTransactionResultsByBlockID", mock.Anything, exeEventReq). 1007 Return(exeEventResp, nil). 1008 Once() 1009 1010 backend, err := New(params) 1011 suite.Require().NoError(err) 1012 1013 // Make the call for the system transaction result 1014 res, err := backend.GetSystemTransactionResult( 1015 context.Background(), 1016 block.ID(), 1017 entities.EventEncodingVersion_CCF_V0, 1018 ) 1019 1020 suite.Require().Nil(res) 1021 suite.Require().Error(err) 1022 suite.Require().Equal(err, status.Errorf(codes.Internal, "failed to convert events from system tx result: %v", 1023 fmt.Errorf("conversion from format JSON_CDC_V0 to CCF_V0 is not supported"))) 1024 }) 1025 } 1026 1027 func (suite *Suite) assertTransactionResultResponse( 1028 err error, 1029 response *acc.TransactionResult, 1030 block flow.Block, 1031 txId flow.Identifier, 1032 txFailed bool, 1033 eventsForTx []flow.Event, 1034 ) { 1035 suite.Require().NoError(err) 1036 suite.Assert().Equal(block.ID(), response.BlockID) 1037 suite.Assert().Equal(block.Header.Height, response.BlockHeight) 1038 suite.Assert().Equal(txId, response.TransactionID) 1039 if txId == suite.systemTx.ID() { 1040 suite.Assert().Equal(flow.ZeroID, response.CollectionID) 1041 } else { 1042 suite.Assert().Equal(block.Payload.Guarantees[0].CollectionID, response.CollectionID) 1043 } 1044 suite.Assert().Equal(len(eventsForTx), len(response.Events)) 1045 // When there are error messages occurred in the transaction, the status should be 1 1046 if txFailed { 1047 suite.Assert().Equal(uint(1), response.StatusCode) 1048 suite.Assert().Equal(expectedErrorMsg, response.ErrorMessage) 1049 } else { 1050 suite.Assert().Equal(uint(0), response.StatusCode) 1051 suite.Assert().Equal("", response.ErrorMessage) 1052 } 1053 suite.Assert().Equal(flow.TransactionStatusSealed, response.Status) 1054 } 1055 1056 // TestTransactionResultFromStorage tests the retrieval of a transaction result (flow.TransactionResult) from storage 1057 // instead of requesting it from the Execution Node. 1058 func (suite *Suite) TestTransactionResultFromStorage() { 1059 // Create fixtures for block, transaction, and collection 1060 block := unittest.BlockFixture() 1061 transaction := unittest.TransactionFixture() 1062 col := flow.CollectionFromTransactions([]*flow.Transaction{&transaction}) 1063 guarantee := col.Guarantee() 1064 block.SetPayload(unittest.PayloadFixture(unittest.WithGuarantees(&guarantee))) 1065 txId := transaction.ID() 1066 blockId := block.ID() 1067 1068 // Mock the behavior of the blocks and transactionResults objects 1069 suite.blocks. 1070 On("ByID", blockId). 1071 Return(&block, nil) 1072 1073 suite.transactionResults.On("ByBlockIDTransactionID", blockId, txId). 1074 Return(&flow.LightTransactionResult{ 1075 TransactionID: txId, 1076 Failed: true, 1077 ComputationUsed: 0, 1078 }, nil) 1079 1080 suite.transactions. 1081 On("ByID", txId). 1082 Return(&transaction.TransactionBody, nil) 1083 1084 // Set up the light collection and mock the behavior of the collections object 1085 lightCol := col.Light() 1086 suite.collections.On("LightByID", col.ID()).Return(&lightCol, nil) 1087 1088 // Set up the events storage mock 1089 totalEvents := 5 1090 eventsForTx := unittest.EventsFixture(totalEvents, flow.EventAccountCreated) 1091 eventMessages := make([]*entities.Event, totalEvents) 1092 for j, event := range eventsForTx { 1093 eventMessages[j] = convert.EventToMessage(event) 1094 } 1095 1096 // expect a call to lookup events by block ID and transaction ID 1097 suite.events.On("ByBlockIDTransactionID", blockId, txId).Return(eventsForTx, nil) 1098 1099 // Set up the state and snapshot mocks 1100 _, fixedENIDs := suite.setupReceipts(&block) 1101 suite.state.On("Final").Return(suite.snapshot, nil).Maybe() 1102 suite.state.On("Sealed").Return(suite.snapshot, nil).Maybe() 1103 suite.snapshot.On("Identities", mock.Anything).Return(fixedENIDs, nil) 1104 suite.snapshot.On("Head", mock.Anything).Return(block.Header, nil) 1105 1106 // create a mock connection factory 1107 connFactory := connectionmock.NewConnectionFactory(suite.T()) 1108 connFactory.On("GetExecutionAPIClient", mock.Anything).Return(suite.execClient, &mockCloser{}, nil) 1109 1110 // create a mock index reporter 1111 reporter := syncmock.NewIndexReporter(suite.T()) 1112 reporter.On("LowestIndexedHeight").Return(block.Header.Height, nil) 1113 reporter.On("HighestIndexedHeight").Return(block.Header.Height+10, nil) 1114 1115 // Set up the backend parameters and the backend instance 1116 params := suite.defaultBackendParams() 1117 // the connection factory should be used to get the execution node client 1118 params.ConnFactory = connFactory 1119 params.FixedExecutionNodeIDs = fixedENIDs.NodeIDs().Strings() 1120 params.TxResultQueryMode = IndexQueryModeLocalOnly 1121 1122 params.EventsIndex = NewEventsIndex(suite.events) 1123 err := params.EventsIndex.Initialize(reporter) 1124 suite.Require().NoError(err) 1125 1126 params.TxResultsIndex = NewTransactionResultsIndex(suite.transactionResults) 1127 err = params.TxResultsIndex.Initialize(reporter) 1128 suite.Require().NoError(err) 1129 1130 backend, err := New(params) 1131 suite.Require().NoError(err) 1132 1133 // Set up the expected error message for the execution node response 1134 1135 exeEventReq := &execproto.GetTransactionErrorMessageRequest{ 1136 BlockId: blockId[:], 1137 TransactionId: txId[:], 1138 } 1139 1140 exeEventResp := &execproto.GetTransactionErrorMessageResponse{ 1141 TransactionId: txId[:], 1142 ErrorMessage: expectedErrorMsg, 1143 } 1144 1145 suite.execClient.On("GetTransactionErrorMessage", mock.Anything, exeEventReq).Return(exeEventResp, nil).Once() 1146 1147 response, err := backend.GetTransactionResult(context.Background(), txId, blockId, flow.ZeroID, entities.EventEncodingVersion_JSON_CDC_V0) 1148 suite.assertTransactionResultResponse(err, response, block, txId, true, eventsForTx) 1149 } 1150 1151 // TestTransactionByIndexFromStorage tests the retrieval of a transaction result (flow.TransactionResult) by index 1152 // and returns it from storage instead of requesting from the Execution Node. 1153 func (suite *Suite) TestTransactionByIndexFromStorage() { 1154 // Create fixtures for block, transaction, and collection 1155 block := unittest.BlockFixture() 1156 transaction := unittest.TransactionFixture() 1157 col := flow.CollectionFromTransactions([]*flow.Transaction{&transaction}) 1158 guarantee := col.Guarantee() 1159 block.SetPayload(unittest.PayloadFixture(unittest.WithGuarantees(&guarantee))) 1160 blockId := block.ID() 1161 txId := transaction.ID() 1162 txIndex := rand.Uint32() 1163 1164 // Set up the light collection and mock the behavior of the collections object 1165 lightCol := col.Light() 1166 suite.collections.On("LightByID", col.ID()).Return(&lightCol, nil) 1167 1168 // Mock the behavior of the blocks and transactionResults objects 1169 suite.blocks. 1170 On("ByID", blockId). 1171 Return(&block, nil) 1172 1173 suite.transactionResults.On("ByBlockIDTransactionIndex", blockId, txIndex). 1174 Return(&flow.LightTransactionResult{ 1175 TransactionID: txId, 1176 Failed: true, 1177 ComputationUsed: 0, 1178 }, nil) 1179 1180 // Set up the events storage mock 1181 totalEvents := 5 1182 eventsForTx := unittest.EventsFixture(totalEvents, flow.EventAccountCreated) 1183 eventMessages := make([]*entities.Event, totalEvents) 1184 for j, event := range eventsForTx { 1185 eventMessages[j] = convert.EventToMessage(event) 1186 } 1187 1188 // expect a call to lookup events by block ID and transaction ID 1189 suite.events.On("ByBlockIDTransactionIndex", blockId, txIndex).Return(eventsForTx, nil) 1190 1191 // Set up the state and snapshot mocks 1192 _, fixedENIDs := suite.setupReceipts(&block) 1193 suite.state.On("Final").Return(suite.snapshot, nil).Maybe() 1194 suite.state.On("Sealed").Return(suite.snapshot, nil).Maybe() 1195 suite.snapshot.On("Identities", mock.Anything).Return(fixedENIDs, nil) 1196 suite.snapshot.On("Head", mock.Anything).Return(block.Header, nil) 1197 1198 // Create a mock connection factory 1199 connFactory := connectionmock.NewConnectionFactory(suite.T()) 1200 connFactory.On("GetExecutionAPIClient", mock.Anything).Return(suite.execClient, &mockCloser{}, nil) 1201 1202 // create a mock index reporter 1203 reporter := syncmock.NewIndexReporter(suite.T()) 1204 reporter.On("LowestIndexedHeight").Return(block.Header.Height, nil) 1205 reporter.On("HighestIndexedHeight").Return(block.Header.Height+10, nil) 1206 1207 // Set up the backend parameters and the backend instance 1208 params := suite.defaultBackendParams() 1209 // the connection factory should be used to get the execution node client 1210 params.ConnFactory = connFactory 1211 params.FixedExecutionNodeIDs = fixedENIDs.NodeIDs().Strings() 1212 params.TxResultQueryMode = IndexQueryModeLocalOnly 1213 1214 params.EventsIndex = NewEventsIndex(suite.events) 1215 err := params.EventsIndex.Initialize(reporter) 1216 suite.Require().NoError(err) 1217 1218 params.TxResultsIndex = NewTransactionResultsIndex(suite.transactionResults) 1219 err = params.TxResultsIndex.Initialize(reporter) 1220 suite.Require().NoError(err) 1221 1222 backend, err := New(params) 1223 suite.Require().NoError(err) 1224 1225 // Set up the expected error message for the execution node response 1226 exeEventReq := &execproto.GetTransactionErrorMessageByIndexRequest{ 1227 BlockId: blockId[:], 1228 Index: txIndex, 1229 } 1230 1231 exeEventResp := &execproto.GetTransactionErrorMessageResponse{ 1232 TransactionId: txId[:], 1233 ErrorMessage: expectedErrorMsg, 1234 } 1235 1236 suite.execClient.On("GetTransactionErrorMessageByIndex", mock.Anything, exeEventReq).Return(exeEventResp, nil).Once() 1237 1238 response, err := backend.GetTransactionResultByIndex(context.Background(), blockId, txIndex, entities.EventEncodingVersion_JSON_CDC_V0) 1239 suite.assertTransactionResultResponse(err, response, block, txId, true, eventsForTx) 1240 } 1241 1242 // TestTransactionResultsByBlockIDFromStorage tests the retrieval of transaction results ([]flow.TransactionResult) 1243 // by block ID from storage instead of requesting from the Execution Node. 1244 func (suite *Suite) TestTransactionResultsByBlockIDFromStorage() { 1245 // Create fixtures for the block and collection 1246 block := unittest.BlockFixture() 1247 col := unittest.CollectionFixture(2) 1248 guarantee := col.Guarantee() 1249 block.SetPayload(unittest.PayloadFixture(unittest.WithGuarantees(&guarantee))) 1250 blockId := block.ID() 1251 1252 // Mock the behavior of the blocks, collections and light transaction results objects 1253 suite.blocks. 1254 On("ByID", blockId). 1255 Return(&block, nil) 1256 lightCol := col.Light() 1257 suite.collections.On("LightByID", mock.Anything).Return(&lightCol, nil) 1258 1259 lightTxResults := make([]flow.LightTransactionResult, len(lightCol.Transactions)) 1260 for i, txID := range lightCol.Transactions { 1261 lightTxResults[i] = flow.LightTransactionResult{ 1262 TransactionID: txID, 1263 Failed: false, 1264 ComputationUsed: 0, 1265 } 1266 } 1267 // simulate the system tx 1268 lightTxResults = append(lightTxResults, flow.LightTransactionResult{ 1269 TransactionID: suite.systemTx.ID(), 1270 Failed: false, 1271 ComputationUsed: 10, 1272 }) 1273 1274 // Mark the first transaction as failed 1275 lightTxResults[0].Failed = true 1276 suite.transactionResults.On("ByBlockID", blockId).Return(lightTxResults, nil) 1277 1278 // Set up the events storage mock 1279 totalEvents := 5 1280 eventsForTx := unittest.EventsFixture(totalEvents, flow.EventAccountCreated) 1281 eventMessages := make([]*entities.Event, totalEvents) 1282 for j, event := range eventsForTx { 1283 eventMessages[j] = convert.EventToMessage(event) 1284 } 1285 1286 // expect a call to lookup events by block ID and transaction ID 1287 suite.events.On("ByBlockIDTransactionID", blockId, mock.Anything).Return(eventsForTx, nil) 1288 1289 // Set up the state and snapshot mocks 1290 _, fixedENIDs := suite.setupReceipts(&block) 1291 suite.state.On("Final").Return(suite.snapshot, nil).Maybe() 1292 suite.state.On("Sealed").Return(suite.snapshot, nil).Maybe() 1293 suite.snapshot.On("Identities", mock.Anything).Return(fixedENIDs, nil) 1294 suite.snapshot.On("Head", mock.Anything).Return(block.Header, nil) 1295 1296 // create a mock connection factory 1297 connFactory := connectionmock.NewConnectionFactory(suite.T()) 1298 connFactory.On("GetExecutionAPIClient", mock.Anything).Return(suite.execClient, &mockCloser{}, nil) 1299 1300 // create a mock index reporter 1301 reporter := syncmock.NewIndexReporter(suite.T()) 1302 reporter.On("LowestIndexedHeight").Return(block.Header.Height, nil) 1303 reporter.On("HighestIndexedHeight").Return(block.Header.Height+10, nil) 1304 1305 // Set up the state and snapshot mocks and the backend instance 1306 params := suite.defaultBackendParams() 1307 // the connection factory should be used to get the execution node client 1308 params.ConnFactory = connFactory 1309 params.FixedExecutionNodeIDs = fixedENIDs.NodeIDs().Strings() 1310 1311 params.EventsIndex = NewEventsIndex(suite.events) 1312 err := params.EventsIndex.Initialize(reporter) 1313 suite.Require().NoError(err) 1314 1315 params.TxResultsIndex = NewTransactionResultsIndex(suite.transactionResults) 1316 err = params.TxResultsIndex.Initialize(reporter) 1317 suite.Require().NoError(err) 1318 1319 params.TxResultQueryMode = IndexQueryModeLocalOnly 1320 1321 backend, err := New(params) 1322 suite.Require().NoError(err) 1323 1324 // Set up the expected error message for the execution node response 1325 exeEventReq := &execproto.GetTransactionErrorMessagesByBlockIDRequest{ 1326 BlockId: blockId[:], 1327 } 1328 1329 res := &execproto.GetTransactionErrorMessagesResponse_Result{ 1330 TransactionId: lightTxResults[0].TransactionID[:], 1331 ErrorMessage: expectedErrorMsg, 1332 Index: 1, 1333 } 1334 exeEventResp := &execproto.GetTransactionErrorMessagesResponse{ 1335 Results: []*execproto.GetTransactionErrorMessagesResponse_Result{ 1336 res, 1337 }, 1338 } 1339 1340 suite.execClient.On("GetTransactionErrorMessagesByBlockID", mock.Anything, exeEventReq).Return(exeEventResp, nil).Once() 1341 1342 response, err := backend.GetTransactionResultsByBlockID(context.Background(), blockId, entities.EventEncodingVersion_JSON_CDC_V0) 1343 suite.Require().NoError(err) 1344 suite.Assert().Equal(len(lightTxResults), len(response)) 1345 1346 // Assertions for each transaction result in the response 1347 for i, responseResult := range response { 1348 lightTx := lightTxResults[i] 1349 suite.assertTransactionResultResponse(err, responseResult, block, lightTx.TransactionID, lightTx.Failed, eventsForTx) 1350 } 1351 }