github.com/onflow/flow-go@v0.33.17/engine/access/rpc/backend/backend_accounts_test.go (about) 1 package backend 2 3 import ( 4 "context" 5 "fmt" 6 "testing" 7 8 "github.com/rs/zerolog" 9 "github.com/stretchr/testify/mock" 10 "github.com/stretchr/testify/suite" 11 "google.golang.org/grpc/codes" 12 "google.golang.org/grpc/status" 13 14 access "github.com/onflow/flow-go/engine/access/mock" 15 connectionmock "github.com/onflow/flow-go/engine/access/rpc/connection/mock" 16 "github.com/onflow/flow-go/engine/common/rpc/convert" 17 "github.com/onflow/flow-go/model/flow" 18 execmock "github.com/onflow/flow-go/module/execution/mock" 19 "github.com/onflow/flow-go/module/irrecoverable" 20 protocol "github.com/onflow/flow-go/state/protocol/mock" 21 "github.com/onflow/flow-go/storage" 22 storagemock "github.com/onflow/flow-go/storage/mock" 23 "github.com/onflow/flow-go/utils/unittest" 24 25 execproto "github.com/onflow/flow/protobuf/go/flow/execution" 26 ) 27 28 type BackendAccountsSuite struct { 29 suite.Suite 30 31 log zerolog.Logger 32 state *protocol.State 33 snapshot *protocol.Snapshot 34 params *protocol.Params 35 rootHeader *flow.Header 36 37 headers *storagemock.Headers 38 receipts *storagemock.ExecutionReceipts 39 connectionFactory *connectionmock.ConnectionFactory 40 chainID flow.ChainID 41 42 executionNodes flow.IdentityList 43 execClient *access.ExecutionAPIClient 44 45 block *flow.Block 46 account *flow.Account 47 failingAddress flow.Address 48 } 49 50 func TestBackendAccountsSuite(t *testing.T) { 51 suite.Run(t, new(BackendAccountsSuite)) 52 } 53 54 func (s *BackendAccountsSuite) SetupTest() { 55 s.log = unittest.Logger() 56 s.state = protocol.NewState(s.T()) 57 s.snapshot = protocol.NewSnapshot(s.T()) 58 s.rootHeader = unittest.BlockHeaderFixture() 59 s.params = protocol.NewParams(s.T()) 60 s.headers = storagemock.NewHeaders(s.T()) 61 s.receipts = storagemock.NewExecutionReceipts(s.T()) 62 s.connectionFactory = connectionmock.NewConnectionFactory(s.T()) 63 s.chainID = flow.Testnet 64 65 s.execClient = access.NewExecutionAPIClient(s.T()) 66 s.executionNodes = unittest.IdentityListFixture(2, unittest.WithRole(flow.RoleExecution)) 67 68 block := unittest.BlockFixture() 69 s.block = &block 70 71 var err error 72 s.account, err = unittest.AccountFixture() 73 s.Require().NoError(err) 74 75 s.failingAddress = unittest.AddressFixture() 76 } 77 78 func (s *BackendAccountsSuite) defaultBackend() *backendAccounts { 79 return &backendAccounts{ 80 log: s.log, 81 state: s.state, 82 headers: s.headers, 83 executionReceipts: s.receipts, 84 connFactory: s.connectionFactory, 85 nodeCommunicator: NewNodeCommunicator(false), 86 } 87 } 88 89 // setupExecutionNodes sets up the mocks required to test against an EN backend 90 func (s *BackendAccountsSuite) setupExecutionNodes(block *flow.Block) { 91 s.params.On("FinalizedRoot").Return(s.rootHeader, nil) 92 s.state.On("Params").Return(s.params) 93 s.state.On("Final").Return(s.snapshot) 94 s.snapshot.On("Identities", mock.Anything).Return(s.executionNodes, nil) 95 96 // this line causes a S1021 lint error because receipts is explicitly declared. this is required 97 // to ensure the mock library handles the response type correctly 98 var receipts flow.ExecutionReceiptList //nolint:gosimple 99 receipts = unittest.ReceiptsForBlockFixture(block, s.executionNodes.NodeIDs()) 100 s.receipts.On("ByBlockID", block.ID()).Return(receipts, nil) 101 102 s.connectionFactory.On("GetExecutionAPIClient", mock.Anything). 103 Return(s.execClient, &mockCloser{}, nil) 104 } 105 106 // setupENSuccessResponse configures the execution node client to return a successful response 107 func (s *BackendAccountsSuite) setupENSuccessResponse(blockID flow.Identifier) { 108 expectedExecRequest := &execproto.GetAccountAtBlockIDRequest{ 109 BlockId: blockID[:], 110 Address: s.account.Address.Bytes(), 111 } 112 113 convertedAccount, err := convert.AccountToMessage(s.account) 114 s.Require().NoError(err) 115 116 s.execClient.On("GetAccountAtBlockID", mock.Anything, expectedExecRequest). 117 Return(&execproto.GetAccountAtBlockIDResponse{ 118 Account: convertedAccount, 119 }, nil) 120 } 121 122 // setupENFailingResponse configures the execution node client to return an error 123 func (s *BackendAccountsSuite) setupENFailingResponse(blockID flow.Identifier, err error) { 124 failingRequest := &execproto.GetAccountAtBlockIDRequest{ 125 BlockId: blockID[:], 126 Address: s.failingAddress.Bytes(), 127 } 128 129 s.execClient.On("GetAccountAtBlockID", mock.Anything, failingRequest). 130 Return(nil, err) 131 } 132 133 // TestGetAccountFromExecutionNode_HappyPath tests successfully getting accounts from execution nodes 134 func (s *BackendAccountsSuite) TestGetAccountFromExecutionNode_HappyPath() { 135 ctx := context.Background() 136 137 s.setupExecutionNodes(s.block) 138 s.setupENSuccessResponse(s.block.ID()) 139 140 backend := s.defaultBackend() 141 backend.scriptExecMode = IndexQueryModeExecutionNodesOnly 142 143 s.Run("GetAccount - happy path", func() { 144 s.testGetAccount(ctx, backend, codes.OK) 145 }) 146 147 s.Run("GetAccountAtLatestBlock - happy path", func() { 148 s.testGetAccountAtLatestBlock(ctx, backend, codes.OK) 149 }) 150 151 s.Run("GetAccountAtBlockHeight - happy path", func() { 152 s.testGetAccountAtBlockHeight(ctx, backend, codes.OK) 153 }) 154 } 155 156 // TestGetAccountFromExecutionNode_Fails errors received from execution nodes are returned 157 func (s *BackendAccountsSuite) TestGetAccountFromExecutionNode_Fails() { 158 ctx := context.Background() 159 160 // use a status code that's not used in the API to make sure it's passed through 161 statusCode := codes.FailedPrecondition 162 errToReturn := status.Error(statusCode, "random error") 163 164 s.setupExecutionNodes(s.block) 165 s.setupENFailingResponse(s.block.ID(), errToReturn) 166 167 backend := s.defaultBackend() 168 backend.scriptExecMode = IndexQueryModeExecutionNodesOnly 169 170 s.Run("GetAccount - fails with backend err", func() { 171 s.testGetAccount(ctx, backend, statusCode) 172 }) 173 174 s.Run("GetAccountAtLatestBlock - fails with backend err", func() { 175 s.testGetAccountAtLatestBlock(ctx, backend, statusCode) 176 }) 177 178 s.Run("GetAccountAtBlockHeight - fails with backend err", func() { 179 s.testGetAccountAtBlockHeight(ctx, backend, statusCode) 180 }) 181 } 182 183 // TestGetAccountFromStorage_HappyPath test successfully getting accounts from local storage 184 func (s *BackendAccountsSuite) TestGetAccountFromStorage_HappyPath() { 185 ctx := context.Background() 186 187 scriptExecutor := execmock.NewScriptExecutor(s.T()) 188 scriptExecutor.On("GetAccountAtBlockHeight", mock.Anything, s.account.Address, s.block.Header.Height). 189 Return(s.account, nil) 190 191 backend := s.defaultBackend() 192 backend.scriptExecMode = IndexQueryModeLocalOnly 193 backend.scriptExecutor = scriptExecutor 194 195 s.Run("GetAccount - happy path", func() { 196 s.testGetAccount(ctx, backend, codes.OK) 197 }) 198 199 s.Run("GetAccountAtLatestBlock - happy path", func() { 200 s.testGetAccountAtLatestBlock(ctx, backend, codes.OK) 201 }) 202 203 s.Run("GetAccountAtBlockHeight - happy path", func() { 204 s.testGetAccountAtBlockHeight(ctx, backend, codes.OK) 205 }) 206 } 207 208 // TestGetAccountFromStorage_Fails tests that errors received from local storage are handled 209 // and converted to the appropriate status code 210 func (s *BackendAccountsSuite) TestGetAccountFromStorage_Fails() { 211 ctx := context.Background() 212 213 scriptExecutor := execmock.NewScriptExecutor(s.T()) 214 215 backend := s.defaultBackend() 216 backend.scriptExecMode = IndexQueryModeLocalOnly 217 backend.scriptExecutor = scriptExecutor 218 219 testCases := []struct { 220 err error 221 statusCode codes.Code 222 }{ 223 { 224 err: storage.ErrHeightNotIndexed, 225 statusCode: codes.OutOfRange, 226 }, 227 { 228 err: storage.ErrNotFound, 229 statusCode: codes.NotFound, 230 }, 231 { 232 err: fmt.Errorf("system error"), 233 statusCode: codes.Internal, 234 }, 235 } 236 237 for _, tt := range testCases { 238 scriptExecutor.On("GetAccountAtBlockHeight", mock.Anything, s.failingAddress, s.block.Header.Height). 239 Return(nil, tt.err).Times(3) 240 241 s.Run(fmt.Sprintf("GetAccount - fails with %v", tt.err), func() { 242 s.testGetAccount(ctx, backend, tt.statusCode) 243 }) 244 245 s.Run(fmt.Sprintf("GetAccountAtLatestBlock - fails with %v", tt.err), func() { 246 s.testGetAccountAtLatestBlock(ctx, backend, tt.statusCode) 247 }) 248 249 s.Run(fmt.Sprintf("GetAccountAtBlockHeight - fails with %v", tt.err), func() { 250 s.testGetAccountAtBlockHeight(ctx, backend, tt.statusCode) 251 }) 252 } 253 } 254 255 // TestGetAccountFromFailover_HappyPath tests that when an error is returned getting an account 256 // from local storage, the backend will attempt to get the account from an execution node 257 func (s *BackendAccountsSuite) TestGetAccountFromFailover_HappyPath() { 258 ctx := context.Background() 259 260 s.setupExecutionNodes(s.block) 261 s.setupENSuccessResponse(s.block.ID()) 262 263 scriptExecutor := execmock.NewScriptExecutor(s.T()) 264 265 backend := s.defaultBackend() 266 backend.scriptExecMode = IndexQueryModeFailover 267 backend.scriptExecutor = scriptExecutor 268 269 for _, errToReturn := range []error{storage.ErrHeightNotIndexed, storage.ErrNotFound} { 270 scriptExecutor.On("GetAccountAtBlockHeight", mock.Anything, s.account.Address, s.block.Header.Height). 271 Return(nil, errToReturn).Times(3) 272 273 s.Run(fmt.Sprintf("GetAccount - happy path - recovers %v", errToReturn), func() { 274 s.testGetAccount(ctx, backend, codes.OK) 275 }) 276 277 s.Run(fmt.Sprintf("GetAccountAtLatestBlock - happy path - recovers %v", errToReturn), func() { 278 s.testGetAccountAtLatestBlock(ctx, backend, codes.OK) 279 }) 280 281 s.Run(fmt.Sprintf("GetAccountAtBlockHeight - happy path - recovers %v", errToReturn), func() { 282 s.testGetAccountAtBlockHeight(ctx, backend, codes.OK) 283 }) 284 } 285 } 286 287 // TestGetAccountFromFailover_ReturnsENErrors tests that when an error is returned from the execution 288 // node during a failover, it is returned to the caller. 289 func (s *BackendAccountsSuite) TestGetAccountFromFailover_ReturnsENErrors() { 290 ctx := context.Background() 291 292 // use a status code that's not used in the API to make sure it's passed through 293 statusCode := codes.FailedPrecondition 294 errToReturn := status.Error(statusCode, "random error") 295 296 s.setupExecutionNodes(s.block) 297 s.setupENFailingResponse(s.block.ID(), errToReturn) 298 299 scriptExecutor := execmock.NewScriptExecutor(s.T()) 300 scriptExecutor.On("GetAccountAtBlockHeight", mock.Anything, s.failingAddress, s.block.Header.Height). 301 Return(nil, storage.ErrHeightNotIndexed) 302 303 backend := s.defaultBackend() 304 backend.scriptExecMode = IndexQueryModeFailover 305 backend.scriptExecutor = scriptExecutor 306 307 s.Run("GetAccount - fails with backend err", func() { 308 s.testGetAccount(ctx, backend, statusCode) 309 }) 310 311 s.Run("GetAccountAtLatestBlock - fails with backend err", func() { 312 s.testGetAccountAtLatestBlock(ctx, backend, statusCode) 313 }) 314 315 s.Run("GetAccountAtBlockHeight - fails with backend err", func() { 316 s.testGetAccountAtBlockHeight(ctx, backend, statusCode) 317 }) 318 } 319 320 // TestGetAccountAtLatestBlock_InconsistentState tests that signaler context received error when node state is 321 // inconsistent 322 func (s *BackendAccountsSuite) TestGetAccountAtLatestBlockFromStorage_InconsistentState() { 323 scriptExecutor := execmock.NewScriptExecutor(s.T()) 324 325 backend := s.defaultBackend() 326 backend.scriptExecMode = IndexQueryModeLocalOnly 327 backend.scriptExecutor = scriptExecutor 328 329 s.Run(fmt.Sprintf("GetAccountAtLatestBlock - fails with %v", "inconsistent node's state"), func() { 330 s.state.On("Sealed").Return(s.snapshot, nil) 331 332 err := fmt.Errorf("inconsistent node's state") 333 s.snapshot.On("Head").Return(nil, err) 334 335 signCtxErr := irrecoverable.NewExceptionf("failed to lookup sealed header: %w", err) 336 signalerCtx := irrecoverable.WithSignalerContext(context.Background(), irrecoverable.NewMockSignalerContextExpectError(s.T(), context.Background(), signCtxErr)) 337 338 actual, err := backend.GetAccountAtLatestBlock(signalerCtx, s.failingAddress) 339 s.Require().Error(err) 340 s.Require().Nil(actual) 341 }) 342 } 343 344 func (s *BackendAccountsSuite) testGetAccount(ctx context.Context, backend *backendAccounts, statusCode codes.Code) { 345 s.state.On("Sealed").Return(s.snapshot, nil).Once() 346 s.snapshot.On("Head").Return(s.block.Header, nil).Once() 347 348 if statusCode == codes.OK { 349 actual, err := backend.GetAccount(ctx, s.account.Address) 350 s.Require().NoError(err) 351 s.Require().Equal(s.account, actual) 352 } else { 353 actual, err := backend.GetAccount(ctx, s.failingAddress) 354 s.Require().Error(err) 355 s.Require().Equal(statusCode, status.Code(err)) 356 s.Require().Nil(actual) 357 } 358 } 359 360 func (s *BackendAccountsSuite) testGetAccountAtLatestBlock(ctx context.Context, backend *backendAccounts, statusCode codes.Code) { 361 s.state.On("Sealed").Return(s.snapshot, nil).Once() 362 s.snapshot.On("Head").Return(s.block.Header, nil).Once() 363 364 if statusCode == codes.OK { 365 actual, err := backend.GetAccountAtLatestBlock(ctx, s.account.Address) 366 s.Require().NoError(err) 367 s.Require().Equal(s.account, actual) 368 } else { 369 actual, err := backend.GetAccountAtLatestBlock(ctx, s.failingAddress) 370 s.Require().Error(err) 371 s.Require().Equal(statusCode, status.Code(err)) 372 s.Require().Nil(actual) 373 } 374 } 375 376 func (s *BackendAccountsSuite) testGetAccountAtBlockHeight(ctx context.Context, backend *backendAccounts, statusCode codes.Code) { 377 height := s.block.Header.Height 378 s.headers.On("BlockIDByHeight", height).Return(s.block.Header.ID(), nil).Once() 379 380 if statusCode == codes.OK { 381 actual, err := backend.GetAccountAtBlockHeight(ctx, s.account.Address, height) 382 s.Require().NoError(err) 383 s.Require().Equal(s.account, actual) 384 } else { 385 actual, err := backend.GetAccountAtBlockHeight(ctx, s.failingAddress, height) 386 s.Require().Error(err) 387 s.Require().Equal(statusCode, status.Code(err)) 388 s.Require().Nil(actual) 389 } 390 }