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