github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/engine/access/rpc/backend/backend_scripts_test.go (about) 1 package backend 2 3 import ( 4 "context" 5 "crypto/md5" //nolint:gosec 6 "fmt" 7 "testing" 8 "time" 9 10 lru "github.com/hashicorp/golang-lru/v2" 11 "github.com/rs/zerolog" 12 "github.com/stretchr/testify/mock" 13 "github.com/stretchr/testify/suite" 14 "google.golang.org/grpc/codes" 15 "google.golang.org/grpc/status" 16 17 execproto "github.com/onflow/flow/protobuf/go/flow/execution" 18 19 access "github.com/onflow/flow-go/engine/access/mock" 20 connectionmock "github.com/onflow/flow-go/engine/access/rpc/connection/mock" 21 fvmerrors "github.com/onflow/flow-go/fvm/errors" 22 "github.com/onflow/flow-go/model/flow" 23 execmock "github.com/onflow/flow-go/module/execution/mock" 24 "github.com/onflow/flow-go/module/irrecoverable" 25 "github.com/onflow/flow-go/module/metrics" 26 protocol "github.com/onflow/flow-go/state/protocol/mock" 27 "github.com/onflow/flow-go/storage" 28 storagemock "github.com/onflow/flow-go/storage/mock" 29 "github.com/onflow/flow-go/utils/unittest" 30 ) 31 32 var ( 33 expectedResponse = []byte("response_data") 34 35 cadenceErr = fvmerrors.NewCodedError(fvmerrors.ErrCodeCadenceRunTimeError, "cadence error") 36 fvmFailureErr = fvmerrors.NewCodedFailure(fvmerrors.FailureCodeBlockFinderFailure, "fvm error") 37 ctxCancelErr = fvmerrors.NewCodedError(fvmerrors.ErrCodeScriptExecutionCancelledError, "context canceled error") 38 timeoutErr = fvmerrors.NewCodedError(fvmerrors.ErrCodeScriptExecutionTimedOutError, "timeout error") 39 compLimitErr = fvmerrors.NewCodedError(fvmerrors.ErrCodeComputationLimitExceededError, "computation limit exceeded error") 40 memLimitErr = fvmerrors.NewCodedError(fvmerrors.ErrCodeMemoryLimitExceededError, "memory limit exceeded error") 41 ) 42 43 // Create a suite similar to GetAccount that covers each of the modes 44 type BackendScriptsSuite struct { 45 suite.Suite 46 47 log zerolog.Logger 48 state *protocol.State 49 snapshot *protocol.Snapshot 50 params *protocol.Params 51 rootHeader *flow.Header 52 53 headers *storagemock.Headers 54 receipts *storagemock.ExecutionReceipts 55 connectionFactory *connectionmock.ConnectionFactory 56 chainID flow.ChainID 57 58 executionNodes flow.IdentityList 59 execClient *access.ExecutionAPIClient 60 61 block *flow.Block 62 63 script []byte 64 arguments [][]byte 65 failingScript []byte 66 } 67 68 func TestBackendScriptsSuite(t *testing.T) { 69 suite.Run(t, new(BackendScriptsSuite)) 70 } 71 72 func (s *BackendScriptsSuite) SetupTest() { 73 s.log = unittest.Logger() 74 s.state = protocol.NewState(s.T()) 75 s.snapshot = protocol.NewSnapshot(s.T()) 76 s.rootHeader = unittest.BlockHeaderFixture() 77 s.params = protocol.NewParams(s.T()) 78 s.headers = storagemock.NewHeaders(s.T()) 79 s.receipts = storagemock.NewExecutionReceipts(s.T()) 80 s.connectionFactory = connectionmock.NewConnectionFactory(s.T()) 81 s.chainID = flow.Testnet 82 83 s.execClient = access.NewExecutionAPIClient(s.T()) 84 s.executionNodes = unittest.IdentityListFixture(2, unittest.WithRole(flow.RoleExecution)) 85 86 block := unittest.BlockFixture() 87 s.block = &block 88 89 s.script = []byte("access(all) fun main() { return 1 }") 90 s.arguments = [][]byte{[]byte("arg1"), []byte("arg2")} 91 s.failingScript = []byte("access(all) fun main() { panic(\"!!\") }") 92 } 93 94 func (s *BackendScriptsSuite) defaultBackend() *backendScripts { 95 loggedScripts, err := lru.New[[md5.Size]byte, time.Time](DefaultLoggedScriptsCacheSize) 96 s.Require().NoError(err) 97 98 return &backendScripts{ 99 log: s.log, 100 metrics: metrics.NewNoopCollector(), 101 state: s.state, 102 headers: s.headers, 103 executionReceipts: s.receipts, 104 loggedScripts: loggedScripts, 105 connFactory: s.connectionFactory, 106 nodeCommunicator: NewNodeCommunicator(false), 107 } 108 } 109 110 // setupExecutionNodes sets up the mocks required to test against an EN backend 111 func (s *BackendScriptsSuite) setupExecutionNodes(block *flow.Block) { 112 s.params.On("FinalizedRoot").Return(s.rootHeader, nil) 113 s.state.On("Params").Return(s.params) 114 s.state.On("Final").Return(s.snapshot) 115 s.snapshot.On("Identities", mock.Anything).Return(s.executionNodes, nil) 116 117 // this line causes a S1021 lint error because receipts is explicitly declared. this is required 118 // to ensure the mock library handles the response type correctly 119 var receipts flow.ExecutionReceiptList //nolint:gosimple 120 receipts = unittest.ReceiptsForBlockFixture(block, s.executionNodes.NodeIDs()) 121 s.receipts.On("ByBlockID", block.ID()).Return(receipts, nil) 122 123 s.connectionFactory.On("GetExecutionAPIClient", mock.Anything). 124 Return(s.execClient, &mockCloser{}, nil) 125 } 126 127 // setupENSuccessResponse configures the execution client mock to return a successful response 128 func (s *BackendScriptsSuite) setupENSuccessResponse(blockID flow.Identifier) { 129 expectedExecRequest := &execproto.ExecuteScriptAtBlockIDRequest{ 130 BlockId: blockID[:], 131 Script: s.script, 132 Arguments: s.arguments, 133 } 134 135 s.execClient.On("ExecuteScriptAtBlockID", mock.Anything, expectedExecRequest). 136 Return(&execproto.ExecuteScriptAtBlockIDResponse{ 137 Value: expectedResponse, 138 }, nil) 139 } 140 141 // setupENFailingResponse configures the execution client mock to return a failing response 142 func (s *BackendScriptsSuite) setupENFailingResponse(blockID flow.Identifier, err error) { 143 expectedExecRequest := &execproto.ExecuteScriptAtBlockIDRequest{ 144 BlockId: blockID[:], 145 Script: s.failingScript, 146 Arguments: s.arguments, 147 } 148 149 s.execClient.On("ExecuteScriptAtBlockID", mock.Anything, expectedExecRequest). 150 Return(nil, err) 151 } 152 153 // TestExecuteScriptOnExecutionNode_HappyPath tests that the backend successfully executes scripts 154 // on execution nodes 155 func (s *BackendScriptsSuite) TestExecuteScriptOnExecutionNode_HappyPath() { 156 ctx := context.Background() 157 158 s.setupExecutionNodes(s.block) 159 s.setupENSuccessResponse(s.block.ID()) 160 161 backend := s.defaultBackend() 162 backend.scriptExecMode = IndexQueryModeExecutionNodesOnly 163 164 s.Run("GetAccount", func() { 165 s.testExecuteScriptAtLatestBlock(ctx, backend, codes.OK) 166 }) 167 168 s.Run("ExecuteScriptAtBlockID", func() { 169 s.testExecuteScriptAtBlockID(ctx, backend, codes.OK) 170 }) 171 172 s.Run("ExecuteScriptAtBlockHeight", func() { 173 s.testExecuteScriptAtBlockHeight(ctx, backend, codes.OK) 174 }) 175 } 176 177 // TestExecuteScriptOnExecutionNode_Fails tests that the backend returns an error when the execution 178 // node returns an error 179 func (s *BackendScriptsSuite) TestExecuteScriptOnExecutionNode_Fails() { 180 ctx := context.Background() 181 182 // use a status code that's not used in the API to make sure it's passed through 183 statusCode := codes.FailedPrecondition 184 errToReturn := status.Error(statusCode, "random error") 185 186 s.setupExecutionNodes(s.block) 187 s.setupENFailingResponse(s.block.ID(), errToReturn) 188 189 backend := s.defaultBackend() 190 backend.scriptExecMode = IndexQueryModeExecutionNodesOnly 191 192 s.Run("GetAccount", func() { 193 s.testExecuteScriptAtLatestBlock(ctx, backend, statusCode) 194 }) 195 196 s.Run("ExecuteScriptAtBlockID", func() { 197 s.testExecuteScriptAtBlockID(ctx, backend, statusCode) 198 }) 199 200 s.Run("ExecuteScriptAtBlockHeight", func() { 201 s.testExecuteScriptAtBlockHeight(ctx, backend, statusCode) 202 }) 203 } 204 205 // TestExecuteScriptFromStorage_HappyPath tests that the backend successfully executes scripts using 206 // the local storage 207 func (s *BackendScriptsSuite) TestExecuteScriptFromStorage_HappyPath() { 208 ctx := context.Background() 209 210 scriptExecutor := execmock.NewScriptExecutor(s.T()) 211 scriptExecutor.On("ExecuteAtBlockHeight", mock.Anything, s.script, s.arguments, s.block.Header.Height). 212 Return(expectedResponse, nil) 213 214 backend := s.defaultBackend() 215 backend.scriptExecMode = IndexQueryModeLocalOnly 216 backend.scriptExecutor = scriptExecutor 217 218 s.Run("GetAccount - happy path", func() { 219 s.testExecuteScriptAtLatestBlock(ctx, backend, codes.OK) 220 }) 221 222 s.Run("GetAccountAtLatestBlock - happy path", func() { 223 s.testExecuteScriptAtBlockID(ctx, backend, codes.OK) 224 }) 225 226 s.Run("GetAccountAtBlockHeight - happy path", func() { 227 s.testExecuteScriptAtBlockHeight(ctx, backend, codes.OK) 228 }) 229 } 230 231 // TestExecuteScriptFromStorage_Fails tests that errors received from local storage are handled 232 // and converted to the appropriate status code 233 func (s *BackendScriptsSuite) TestExecuteScriptFromStorage_Fails() { 234 ctx := context.Background() 235 236 scriptExecutor := execmock.NewScriptExecutor(s.T()) 237 238 backend := s.defaultBackend() 239 backend.scriptExecMode = IndexQueryModeLocalOnly 240 backend.scriptExecutor = scriptExecutor 241 242 testCases := []struct { 243 err error 244 statusCode codes.Code 245 }{ 246 { 247 err: storage.ErrHeightNotIndexed, 248 statusCode: codes.OutOfRange, 249 }, 250 { 251 err: storage.ErrNotFound, 252 statusCode: codes.NotFound, 253 }, 254 { 255 err: fmt.Errorf("system error"), 256 statusCode: codes.Internal, 257 }, 258 { 259 err: cadenceErr, 260 statusCode: codes.InvalidArgument, 261 }, 262 { 263 err: fvmFailureErr, 264 statusCode: codes.Internal, 265 }, 266 } 267 268 for _, tt := range testCases { 269 scriptExecutor.On("ExecuteAtBlockHeight", mock.Anything, s.failingScript, s.arguments, s.block.Header.Height). 270 Return(nil, tt.err).Times(3) 271 272 s.Run(fmt.Sprintf("GetAccount - fails with %v", tt.err), func() { 273 s.testExecuteScriptAtLatestBlock(ctx, backend, tt.statusCode) 274 }) 275 276 s.Run(fmt.Sprintf("GetAccountAtLatestBlock - fails with %v", tt.err), func() { 277 s.testExecuteScriptAtBlockID(ctx, backend, tt.statusCode) 278 }) 279 280 s.Run(fmt.Sprintf("GetAccountAtBlockHeight - fails with %v", tt.err), func() { 281 s.testExecuteScriptAtBlockHeight(ctx, backend, tt.statusCode) 282 }) 283 } 284 } 285 286 // TestExecuteScriptWithFailover_HappyPath tests that when an error is returned executing a script 287 // from local storage, the backend will attempt to run it on an execution node 288 func (s *BackendScriptsSuite) TestExecuteScriptWithFailover_HappyPath() { 289 ctx := context.Background() 290 291 errors := []error{ 292 storage.ErrHeightNotIndexed, 293 storage.ErrNotFound, 294 fmt.Errorf("system error"), 295 fvmFailureErr, 296 compLimitErr, 297 memLimitErr, 298 } 299 300 s.setupExecutionNodes(s.block) 301 s.setupENSuccessResponse(s.block.ID()) 302 303 scriptExecutor := execmock.NewScriptExecutor(s.T()) 304 305 backend := s.defaultBackend() 306 backend.scriptExecMode = IndexQueryModeFailover 307 backend.scriptExecutor = scriptExecutor 308 309 for _, errToReturn := range errors { 310 // configure local script executor to fail 311 scriptExecutor.On("ExecuteAtBlockHeight", mock.Anything, s.script, s.arguments, s.block.Header.Height). 312 Return(nil, errToReturn).Times(3) 313 314 s.Run(fmt.Sprintf("ExecuteScriptAtLatestBlock - recovers %v", errToReturn), func() { 315 s.testExecuteScriptAtLatestBlock(ctx, backend, codes.OK) 316 }) 317 318 s.Run(fmt.Sprintf("ExecuteScriptAtBlockID - recovers %v", errToReturn), func() { 319 s.testExecuteScriptAtBlockID(ctx, backend, codes.OK) 320 }) 321 322 s.Run(fmt.Sprintf("ExecuteScriptAtBlockHeight - recovers %v", errToReturn), func() { 323 s.testExecuteScriptAtBlockHeight(ctx, backend, codes.OK) 324 }) 325 } 326 } 327 328 // TestExecuteScriptWithFailover_SkippedForCorrectCodes tests that failover is skipped for 329 // FVM errors that result in InvalidArgument or Canceled errors 330 func (s *BackendScriptsSuite) TestExecuteScriptWithFailover_SkippedForCorrectCodes() { 331 ctx := context.Background() 332 333 // configure local script executor to fail 334 scriptExecutor := execmock.NewScriptExecutor(s.T()) 335 336 backend := s.defaultBackend() 337 backend.scriptExecMode = IndexQueryModeFailover 338 backend.scriptExecutor = scriptExecutor 339 340 testCases := []struct { 341 err error 342 statusCode codes.Code 343 }{ 344 { 345 err: cadenceErr, 346 statusCode: codes.InvalidArgument, 347 }, 348 { 349 err: ctxCancelErr, 350 statusCode: codes.Canceled, 351 }, 352 } 353 354 for _, tt := range testCases { 355 scriptExecutor.On("ExecuteAtBlockHeight", mock.Anything, s.failingScript, s.arguments, s.block.Header.Height). 356 Return(nil, tt.err). 357 Times(3) 358 359 s.Run(fmt.Sprintf("ExecuteScriptAtLatestBlock - %s", tt.statusCode), func() { 360 s.testExecuteScriptAtLatestBlock(ctx, backend, tt.statusCode) 361 }) 362 363 s.Run(fmt.Sprintf("ExecuteScriptAtBlockID - %s", tt.statusCode), func() { 364 s.testExecuteScriptAtBlockID(ctx, backend, tt.statusCode) 365 }) 366 367 s.Run(fmt.Sprintf("ExecuteScriptAtBlockHeight - %s", tt.statusCode), func() { 368 s.testExecuteScriptAtBlockHeight(ctx, backend, tt.statusCode) 369 }) 370 } 371 } 372 373 // TestExecuteScriptWithFailover_ReturnsENErrors tests that when an error is returned from the execution 374 // node during a failover, it is returned to the caller. 375 func (s *BackendScriptsSuite) TestExecuteScriptWithFailover_ReturnsENErrors() { 376 ctx := context.Background() 377 378 // use a status code that's not used in the API to make sure it's passed through 379 statusCode := codes.FailedPrecondition 380 errToReturn := status.Error(statusCode, "random error") 381 382 // setup the execution client mocks 383 s.setupExecutionNodes(s.block) 384 s.setupENFailingResponse(s.block.ID(), errToReturn) 385 386 // configure local script executor to fail 387 scriptExecutor := execmock.NewScriptExecutor(s.T()) 388 scriptExecutor.On("ExecuteAtBlockHeight", mock.Anything, mock.Anything, mock.Anything, s.block.Header.Height). 389 Return(nil, storage.ErrHeightNotIndexed) 390 391 backend := s.defaultBackend() 392 backend.scriptExecMode = IndexQueryModeFailover 393 backend.scriptExecutor = scriptExecutor 394 395 s.Run("ExecuteScriptAtLatestBlock", func() { 396 s.testExecuteScriptAtLatestBlock(ctx, backend, statusCode) 397 }) 398 399 s.Run("ExecuteScriptAtBlockID", func() { 400 s.testExecuteScriptAtBlockID(ctx, backend, statusCode) 401 }) 402 403 s.Run("ExecuteScriptAtBlockHeight", func() { 404 s.testExecuteScriptAtBlockHeight(ctx, backend, statusCode) 405 }) 406 } 407 408 // TestExecuteScriptAtLatestBlockFromStorage_InconsistentState tests that signaler context received error when node state is 409 // inconsistent 410 func (s *BackendScriptsSuite) TestExecuteScriptAtLatestBlockFromStorage_InconsistentState() { 411 scriptExecutor := execmock.NewScriptExecutor(s.T()) 412 413 backend := s.defaultBackend() 414 backend.scriptExecMode = IndexQueryModeLocalOnly 415 backend.scriptExecutor = scriptExecutor 416 417 s.Run(fmt.Sprintf("ExecuteScriptAtLatestBlock - fails with %v", "inconsistent node's state"), func() { 418 s.state.On("Sealed").Return(s.snapshot, nil) 419 420 err := fmt.Errorf("inconsistent node's state") 421 s.snapshot.On("Head").Return(nil, err) 422 423 signCtxErr := irrecoverable.NewExceptionf("failed to lookup sealed header: %w", err) 424 signalerCtx := irrecoverable.WithSignalerContext(context.Background(), 425 irrecoverable.NewMockSignalerContextExpectError(s.T(), context.Background(), signCtxErr)) 426 427 actual, err := backend.ExecuteScriptAtLatestBlock(signalerCtx, s.script, s.arguments) 428 s.Require().Error(err) 429 s.Require().Nil(actual) 430 }) 431 } 432 433 func (s *BackendScriptsSuite) testExecuteScriptAtLatestBlock(ctx context.Context, backend *backendScripts, statusCode codes.Code) { 434 s.state.On("Sealed").Return(s.snapshot, nil).Once() 435 s.snapshot.On("Head").Return(s.block.Header, nil).Once() 436 437 if statusCode == codes.OK { 438 actual, err := backend.ExecuteScriptAtLatestBlock(ctx, s.script, s.arguments) 439 s.Require().NoError(err) 440 s.Require().Equal(expectedResponse, actual) 441 } else { 442 actual, err := backend.ExecuteScriptAtLatestBlock(ctx, s.failingScript, s.arguments) 443 s.Require().Error(err) 444 s.Require().Equal(statusCode, status.Code(err), "error code mismatch: expected %d, got %d: %s", statusCode, status.Code(err), err) 445 s.Require().Nil(actual) 446 } 447 } 448 449 func (s *BackendScriptsSuite) testExecuteScriptAtBlockID(ctx context.Context, backend *backendScripts, statusCode codes.Code) { 450 blockID := s.block.ID() 451 s.headers.On("ByBlockID", blockID).Return(s.block.Header, nil).Once() 452 453 if statusCode == codes.OK { 454 actual, err := backend.ExecuteScriptAtBlockID(ctx, blockID, s.script, s.arguments) 455 s.Require().NoError(err) 456 s.Require().Equal(expectedResponse, actual) 457 } else { 458 actual, err := backend.ExecuteScriptAtBlockID(ctx, blockID, s.failingScript, s.arguments) 459 s.Require().Error(err) 460 s.Require().Equal(statusCode, status.Code(err), "error code mismatch: expected %d, got %d: %s", statusCode, status.Code(err), err) 461 s.Require().Nil(actual) 462 } 463 } 464 465 func (s *BackendScriptsSuite) testExecuteScriptAtBlockHeight(ctx context.Context, backend *backendScripts, statusCode codes.Code) { 466 height := s.block.Header.Height 467 s.headers.On("ByHeight", height).Return(s.block.Header, nil).Once() 468 469 if statusCode == codes.OK { 470 actual, err := backend.ExecuteScriptAtBlockHeight(ctx, height, s.script, s.arguments) 471 s.Require().NoError(err) 472 s.Require().Equal(expectedResponse, actual) 473 } else { 474 actual, err := backend.ExecuteScriptAtBlockHeight(ctx, height, s.failingScript, s.arguments) 475 s.Require().Error(err) 476 s.Require().Equalf(statusCode, status.Code(err), "error code mismatch: expected %d, got %d: %s", statusCode, status.Code(err), err) 477 s.Require().Nil(actual) 478 } 479 }