code.vegaprotocol.io/vega@v0.79.0/wallet/api/client_check_transaction_test.go (about) 1 // Copyright (C) 2023 Gobalsky Labs Limited 2 // 3 // This program is free software: you can redistribute it and/or modify 4 // it under the terms of the GNU Affero General Public License as 5 // published by the Free Software Foundation, either version 3 of the 6 // License, or (at your option) any later version. 7 // 8 // This program is distributed in the hope that it will be useful, 9 // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 // GNU Affero General Public License for more details. 12 // 13 // You should have received a copy of the GNU Affero General Public License 14 // along with this program. If not, see <http://www.gnu.org/licenses/>. 15 16 package api_test 17 18 import ( 19 "context" 20 "errors" 21 "fmt" 22 "sync" 23 "testing" 24 "time" 25 26 "code.vegaprotocol.io/vega/libs/jsonrpc" 27 vgrand "code.vegaprotocol.io/vega/libs/rand" 28 commandspb "code.vegaprotocol.io/vega/protos/vega/commands/v1" 29 "code.vegaprotocol.io/vega/wallet/api" 30 "code.vegaprotocol.io/vega/wallet/api/mocks" 31 nodemocks "code.vegaprotocol.io/vega/wallet/api/node/mocks" 32 "code.vegaprotocol.io/vega/wallet/api/node/types" 33 "code.vegaprotocol.io/vega/wallet/wallet" 34 35 "github.com/golang/mock/gomock" 36 "github.com/stretchr/testify/assert" 37 "github.com/stretchr/testify/require" 38 ) 39 40 func TestClientCheckTransaction(t *testing.T) { 41 t.Run("Documentation matches the code", testClientCheckTransactionSchemaCorrect) 42 t.Run("Checking a transaction with invalid params fails", testCheckingTransactionWithInvalidParamsFails) 43 t.Run("Checking a transaction with valid params succeeds", testCheckingTransactionWithValidParamsSucceeds) 44 t.Run("Checking a transaction in parallel blocks on same party but not on different parties", testCheckingTransactionInParallelBlocksOnSamePartyButNotOnDifferentParties) 45 t.Run("Checking a transaction without the needed permissions check the transaction", testCheckingTransactionWithoutNeededPermissionsDoesNotCheckTransaction) 46 t.Run("Refusing the checking of a transaction does not check the transaction", testRefusingCheckingOfTransactionDoesNotCheckTransaction) 47 t.Run("Cancelling the review does not check the transaction", testCancellingTheReviewDoesNotCheckTransaction) 48 t.Run("Interrupting the request does not check the transaction", testInterruptingTheRequestDoesNotCheckTransaction) 49 t.Run("Getting internal error during the review does not check the transaction", testGettingInternalErrorDuringReviewDoesNotCheckTransaction) 50 t.Run("No healthy node available does not check the transaction", testNoHealthyNodeAvailableDoesNotCheckTransaction) 51 t.Run("Failing to get the spam statistics does not check the transaction", testFailingToGetSpamStatsDoesNotCheckTransaction) 52 t.Run("Failure when checking transaction returns an error", testFailureWhenCheckingTransactionReturnsAnError) 53 t.Run("Failing spam checks aborts the transaction", testFailingSpamChecksAbortsCheckingTheTransaction) 54 } 55 56 func testClientCheckTransactionSchemaCorrect(t *testing.T) { 57 assertEqualSchema(t, "client.check_transaction", api.ClientCheckTransactionParams{}, api.ClientCheckTransactionResult{}) 58 } 59 60 func testCheckingTransactionWithInvalidParamsFails(t *testing.T) { 61 tcs := []struct { 62 name string 63 params interface{} 64 expectedError error 65 }{ 66 { 67 name: "with nil params", 68 params: nil, 69 expectedError: api.ErrParamsRequired, 70 }, 71 { 72 name: "with wrong type of params", 73 params: "test", 74 expectedError: api.ErrParamsDoNotMatch, 75 }, 76 { 77 name: "with empty public key permissions", 78 params: api.ClientCheckTransactionParams{ 79 PublicKey: "", 80 Transaction: testTransaction(t), 81 }, 82 expectedError: api.ErrPublicKeyIsRequired, 83 }, 84 { 85 name: "with no transaction", 86 params: api.ClientCheckTransactionParams{ 87 PublicKey: vgrand.RandomStr(10), 88 Transaction: nil, 89 }, 90 expectedError: api.ErrTransactionIsRequired, 91 }, 92 { 93 name: "with transaction as invalid Vega command", 94 params: api.ClientCheckTransactionParams{ 95 PublicKey: vgrand.RandomStr(10), 96 Transaction: map[string]interface{}{ 97 "type": "not vega command", 98 }, 99 }, 100 expectedError: errors.New("the transaction does not use a valid Vega command: unknown field \"type\" in vega.wallet.v1.SubmitTransactionRequest"), 101 }, 102 } 103 104 for _, tc := range tcs { 105 t.Run(tc.name, func(tt *testing.T) { 106 // given 107 ctx, _ := clientContextForTest() 108 hostname := vgrand.RandomStr(5) 109 wallet1 := walletWithPerms(t, hostname, wallet.Permissions{ 110 PublicKeys: wallet.PublicKeysPermission{ 111 Access: wallet.ReadAccess, 112 AllowedKeys: nil, 113 }, 114 }) 115 connectedWallet, err := api.NewConnectedWallet(hostname, wallet1) 116 if err != nil { 117 t.Fatalf(err.Error()) 118 } 119 120 // setup 121 handler := newCheckTransactionHandler(tt) 122 123 // when 124 result, errorDetails := handler.handle(t, ctx, tc.params, connectedWallet) 125 126 // then 127 require.Empty(tt, result) 128 assertInvalidParams(tt, errorDetails, tc.expectedError) 129 }) 130 } 131 } 132 133 func testCheckingTransactionWithValidParamsSucceeds(t *testing.T) { 134 // given 135 ctx, traceID := clientContextForTest() 136 hostname := vgrand.RandomStr(5) 137 wallet1 := walletWithPerms(t, hostname, wallet.Permissions{ 138 PublicKeys: wallet.PublicKeysPermission{ 139 Access: wallet.ReadAccess, 140 AllowedKeys: nil, 141 }, 142 }) 143 kp, err := wallet1.GenerateKeyPair(nil) 144 if err != nil { 145 t.Fatalf(err.Error()) 146 } 147 connectedWallet, err := api.NewConnectedWallet(hostname, wallet1) 148 if err != nil { 149 t.Fatalf(err.Error()) 150 } 151 spamStats := types.SpamStatistics{ 152 ChainID: vgrand.RandomStr(5), 153 LastBlockHeight: 100, 154 Proposals: &types.SpamStatistic{MaxForEpoch: 1}, 155 NodeAnnouncements: &types.SpamStatistic{MaxForEpoch: 1}, 156 Delegations: &types.SpamStatistic{MaxForEpoch: 1}, 157 Transfers: &types.SpamStatistic{MaxForEpoch: 1}, 158 Votes: &types.VoteSpamStatistics{MaxForEpoch: 1}, 159 PoW: &types.PoWStatistics{ 160 PowBlockStates: []types.PoWBlockState{{}}, 161 }, 162 } 163 164 // setup 165 handler := newCheckTransactionHandler(t) 166 167 // -- expected calls 168 handler.interactor.EXPECT().NotifyInteractionSessionBegan(ctx, traceID, api.TransactionReviewWorkflow, uint8(2)).Times(1).Return(nil) 169 handler.interactor.EXPECT().NotifyInteractionSessionEnded(ctx, traceID).Times(1) 170 handler.walletStore.EXPECT().GetWallet(ctx, wallet1.Name()).Times(1).Return(wallet1, nil) 171 handler.interactor.EXPECT().RequestTransactionReviewForChecking(ctx, traceID, uint8(1), hostname, wallet1.Name(), kp.PublicKey(), fakeTransaction, gomock.Any()).Times(1).Return(true, nil) 172 handler.nodeSelector.EXPECT().Node(ctx, gomock.Any()).Times(1).Return(handler.node, nil) 173 handler.node.EXPECT().SpamStatistics(ctx, kp.PublicKey()).Times(1).Return(spamStats, nil) 174 handler.spam.EXPECT().CheckSubmission(gomock.Any(), &spamStats).Times(1).Return(nil) 175 handler.interactor.EXPECT().NotifySuccessfulRequest(ctx, traceID, uint8(2), api.TransactionSuccessfullyChecked).Times(1) 176 handler.spam.EXPECT().GenerateProofOfWork(kp.PublicKey(), gomock.Any()).Times(1).Return(&commandspb.ProofOfWork{ 177 Tid: vgrand.RandomStr(5), 178 Nonce: 12345678, 179 }, nil) 180 handler.node.EXPECT().CheckTransaction(ctx, gomock.Any()).Times(1).Return(nil) 181 handler.interactor.EXPECT().Log(ctx, traceID, gomock.Any(), gomock.Any()).AnyTimes() 182 183 // when 184 result, errorDetails := handler.handle(t, ctx, api.ClientCheckTransactionParams{ 185 PublicKey: kp.PublicKey(), 186 Transaction: testTransaction(t), 187 }, connectedWallet) 188 189 // then 190 assert.Nil(t, errorDetails) 191 require.NotEmpty(t, result) 192 assert.NotEmpty(t, result.Transaction) 193 } 194 195 func testCheckingTransactionInParallelBlocksOnSamePartyButNotOnDifferentParties(t *testing.T) { 196 // setup 197 198 // Use channels to orchestrate requests. 199 sendSecondRequests := make(chan interface{}) 200 sendThirdRequests := make(chan interface{}) 201 waitForSecondRequestToExit := make(chan interface{}) 202 waitForThirdRequestToExit := make(chan interface{}) 203 204 hostname := vgrand.RandomStr(5) 205 206 // One context for each request. 207 r1Ctx, r1TraceID := clientContextForTest() 208 r2Ctx, _ := clientContextForTest() 209 r3Ctx, r3TraceID := clientContextForTest() 210 211 // A wallet with 2 keys to have 2 different parties. 212 wallet1 := walletWithPerms(t, hostname, wallet.Permissions{ 213 PublicKeys: wallet.PublicKeysPermission{ 214 Access: wallet.ReadAccess, 215 AllowedKeys: nil, 216 }, 217 }) 218 kp1, err := wallet1.GenerateKeyPair(nil) 219 require.NoError(t, err) 220 kp2, err := wallet1.GenerateKeyPair(nil) 221 require.NoError(t, err) 222 223 // We can have a single connection as the implementation only cares about the 224 // party. 225 connectedWallet, err := api.NewConnectedWallet(hostname, wallet1) 226 require.NoError(t, err) 227 228 // Some mock data. Their value is irrelevant to test parallelism, so we recycle 229 // them. 230 spamStats := types.SpamStatistics{ 231 ChainID: vgrand.RandomStr(5), 232 LastBlockHeight: 100, 233 Proposals: &types.SpamStatistic{MaxForEpoch: 1}, 234 NodeAnnouncements: &types.SpamStatistic{MaxForEpoch: 1}, 235 Delegations: &types.SpamStatistic{MaxForEpoch: 1}, 236 Transfers: &types.SpamStatistic{MaxForEpoch: 1}, 237 Votes: &types.VoteSpamStatistics{MaxForEpoch: 1}, 238 PoW: &types.PoWStatistics{ 239 PowBlockStates: []types.PoWBlockState{{}}, 240 }, 241 } 242 pow := &commandspb.ProofOfWork{ 243 Tid: vgrand.RandomStr(5), 244 Nonce: 12345678, 245 } 246 247 // Setting up the mocked calls. The second request shouldn't trigger any of 248 // them, since it should be rejected because it uses the same party as the 249 // first request, which only unblock at the end. 250 handler := newCheckTransactionHandler(t) 251 252 gomock.InOrder( 253 // First request. 254 handler.spam.EXPECT().GenerateProofOfWork(kp1.PublicKey(), &spamStats).Times(1).Return(pow, nil), 255 // Third request. 256 handler.spam.EXPECT().GenerateProofOfWork(kp2.PublicKey(), &spamStats).Times(1).Return(pow, nil), 257 ) 258 gomock.InOrder( 259 // First request. 260 handler.interactor.EXPECT().NotifyInteractionSessionBegan(r1Ctx, r1TraceID, api.TransactionReviewWorkflow, uint8(2)).Times(1).Return(nil), 261 // Third request. 262 handler.interactor.EXPECT().NotifyInteractionSessionBegan(r3Ctx, r3TraceID, api.TransactionReviewWorkflow, uint8(2)).Times(1).Return(nil), 263 ) 264 gomock.InOrder( 265 // Third request is expected before because the first request get unblocked 266 // when the third request finishes. 267 handler.interactor.EXPECT().NotifyInteractionSessionEnded(r3Ctx, r3TraceID).Times(1), 268 // First request. 269 handler.interactor.EXPECT().NotifyInteractionSessionEnded(r1Ctx, r1TraceID).Times(1), 270 ) 271 gomock.InOrder( 272 // First request. 273 handler.interactor.EXPECT().RequestTransactionReviewForChecking(r1Ctx, r1TraceID, uint8(1), hostname, wallet1.Name(), kp1.PublicKey(), fakeTransaction, gomock.Any()).Times(1).Return(true, nil), 274 // Third request. 275 handler.interactor.EXPECT().RequestTransactionReviewForChecking(r3Ctx, r3TraceID, uint8(1), hostname, wallet1.Name(), kp2.PublicKey(), fakeTransaction, gomock.Any()).Times(1).Return(true, nil), 276 ) 277 gomock.InOrder( 278 // First request. 279 handler.nodeSelector.EXPECT().Node(r1Ctx, gomock.Any()).Times(1).Return(handler.node, nil), 280 // Third request. 281 handler.nodeSelector.EXPECT().Node(r3Ctx, gomock.Any()).Times(1).Return(handler.node, nil), 282 ) 283 gomock.InOrder( 284 // First request. 285 handler.walletStore.EXPECT().GetWallet(r1Ctx, wallet1.Name()).Times(1).Return(wallet1, nil), 286 // Second request. 287 handler.walletStore.EXPECT().GetWallet(r2Ctx, wallet1.Name()).Times(1).DoAndReturn(func(_ context.Context, _ string) (wallet.Wallet, error) { 288 close(sendThirdRequests) 289 return wallet1, nil 290 }), 291 // Third request. 292 handler.walletStore.EXPECT().GetWallet(r3Ctx, wallet1.Name()).Times(1).Return(wallet1, nil), 293 ) 294 gomock.InOrder( 295 // First request. 296 handler.node.EXPECT().SpamStatistics(r1Ctx, kp1.PublicKey()).Times(1).Return(spamStats, nil), 297 // Third request. 298 handler.node.EXPECT().SpamStatistics(r3Ctx, kp2.PublicKey()).Times(1).Return(spamStats, nil), 299 ) 300 gomock.InOrder( 301 // First request. 302 handler.spam.EXPECT().CheckSubmission(gomock.Any(), &spamStats).Times(1).Return(nil), 303 // Third request. 304 handler.spam.EXPECT().CheckSubmission(gomock.Any(), &spamStats).Times(1).Return(nil), 305 ) 306 gomock.InOrder( 307 // First request. 308 handler.interactor.EXPECT().NotifySuccessfulRequest(r1Ctx, r1TraceID, uint8(2), api.TransactionSuccessfullyChecked).Times(1).Do(func(_ context.Context, _ string, _ uint8, _ string) { 309 // Unblock the second and third requests, and trigger the signing. 310 close(sendSecondRequests) 311 <-waitForSecondRequestToExit 312 }), 313 // Third request. 314 handler.interactor.EXPECT().NotifySuccessfulRequest(r3Ctx, r3TraceID, uint8(2), api.TransactionSuccessfullyChecked).Times(1), 315 ) 316 gomock.InOrder( 317 // First request. 318 handler.node.EXPECT().CheckTransaction(r1Ctx, gomock.Any()).AnyTimes().Return(nil), 319 // Third request. 320 handler.node.EXPECT().CheckTransaction(r3Ctx, gomock.Any()).AnyTimes().Return(nil), 321 ) 322 gomock.InOrder( 323 // First request. 324 handler.interactor.EXPECT().Log(r1Ctx, r1TraceID, gomock.Any(), gomock.Any()).AnyTimes(), 325 // Third request. 326 handler.interactor.EXPECT().Log(r3Ctx, r3TraceID, gomock.Any(), gomock.Any()).AnyTimes(), 327 ) 328 329 wg := sync.WaitGroup{} 330 wg.Add(3) 331 332 go func() { 333 defer wg.Done() 334 // when 335 result, errorDetails := handler.handle(t, r1Ctx, api.ClientCheckTransactionParams{ 336 PublicKey: kp1.PublicKey(), 337 Transaction: testTransaction(t), 338 }, connectedWallet) 339 340 <-waitForSecondRequestToExit 341 <-waitForThirdRequestToExit 342 343 // then 344 assert.Nil(t, errorDetails) 345 require.NotEmpty(t, result) 346 assert.NotEmpty(t, result.Transaction) 347 }() 348 349 go func() { 350 defer wg.Done() 351 352 // Closing this resume, unblock the first request. 353 defer close(waitForSecondRequestToExit) 354 355 // Ensure the first request acquire the "lock" on the public key. 356 <-sendSecondRequests 357 358 // when 359 result, errorDetails := handler.handle(t, r2Ctx, api.ClientCheckTransactionParams{ 360 PublicKey: kp1.PublicKey(), 361 Transaction: testTransaction(t), 362 }, connectedWallet) 363 364 // then 365 assert.NotNil(t, errorDetails) 366 assertRequestNotPermittedError(t, errorDetails, fmt.Errorf("this public key %q is already in use, retry later", kp1.PublicKey())) 367 require.Empty(t, result) 368 }() 369 370 go func() { 371 defer wg.Done() 372 defer close(waitForThirdRequestToExit) 373 374 // Ensure the first request acquire the "lock" on the public key, and 375 // we second request calls `GetWallet()` before the third request. 376 <-sendThirdRequests 377 378 // then 379 result, errorDetails := handler.handle(t, r3Ctx, api.ClientCheckTransactionParams{ 380 PublicKey: kp2.PublicKey(), 381 Transaction: testTransaction(t), 382 }, connectedWallet) 383 384 // then 385 assert.Nil(t, errorDetails) 386 require.NotEmpty(t, result) 387 assert.NotEmpty(t, result.Transaction) 388 }() 389 390 wg.Wait() 391 } 392 393 func testCheckingTransactionWithoutNeededPermissionsDoesNotCheckTransaction(t *testing.T) { 394 // given 395 ctx, _ := clientContextForTest() 396 hostname := vgrand.RandomStr(5) 397 wallet1 := walletWithPerms(t, hostname, wallet.Permissions{}) 398 kp, err := wallet1.GenerateKeyPair(nil) 399 if err != nil { 400 t.Fatalf(err.Error()) 401 } 402 connectedWallet, err := api.NewConnectedWallet(hostname, wallet1) 403 if err != nil { 404 t.Fatalf(err.Error()) 405 } 406 407 // setup 408 handler := newCheckTransactionHandler(t) 409 410 // when 411 result, errorDetails := handler.handle(t, ctx, api.ClientCheckTransactionParams{ 412 PublicKey: kp.PublicKey(), 413 Transaction: testTransaction(t), 414 }, connectedWallet) 415 416 // then 417 assertRequestNotPermittedError(t, errorDetails, api.ErrPublicKeyIsNotAllowedToBeUsed) 418 assert.Empty(t, result) 419 } 420 421 func testRefusingCheckingOfTransactionDoesNotCheckTransaction(t *testing.T) { 422 // given 423 ctx, traceID := clientContextForTest() 424 hostname := vgrand.RandomStr(5) 425 wallet1 := walletWithPerms(t, hostname, wallet.Permissions{ 426 PublicKeys: wallet.PublicKeysPermission{ 427 Access: wallet.ReadAccess, 428 AllowedKeys: nil, 429 }, 430 }) 431 kp, err := wallet1.GenerateKeyPair(nil) 432 if err != nil { 433 t.Fatalf(err.Error()) 434 } 435 connectedWallet, err := api.NewConnectedWallet(hostname, wallet1) 436 if err != nil { 437 t.Fatalf(err.Error()) 438 } 439 440 // setup 441 handler := newCheckTransactionHandler(t) 442 // -- expected calls 443 handler.walletStore.EXPECT().GetWallet(ctx, wallet1.Name()).Times(1).Return(wallet1, nil) 444 handler.interactor.EXPECT().RequestTransactionReviewForChecking(ctx, traceID, uint8(1), hostname, wallet1.Name(), kp.PublicKey(), fakeTransaction, gomock.Any()).Times(1).Return(false, nil) 445 handler.interactor.EXPECT().NotifyInteractionSessionBegan(ctx, traceID, api.TransactionReviewWorkflow, uint8(2)).Times(1).Return(nil) 446 handler.interactor.EXPECT().NotifyInteractionSessionEnded(ctx, gomock.Any()).Times(1) 447 448 // when 449 result, errorDetails := handler.handle(t, ctx, api.ClientCheckTransactionParams{ 450 PublicKey: kp.PublicKey(), 451 Transaction: testTransaction(t), 452 }, connectedWallet) 453 454 // then 455 assertUserRejectionError(t, errorDetails, api.ErrUserRejectedCheckingOfTransaction) 456 assert.Empty(t, result) 457 } 458 459 func testCancellingTheReviewDoesNotCheckTransaction(t *testing.T) { 460 // given 461 ctx, traceID := clientContextForTest() 462 hostname := vgrand.RandomStr(5) 463 wallet1 := walletWithPerms(t, hostname, wallet.Permissions{ 464 PublicKeys: wallet.PublicKeysPermission{ 465 Access: wallet.ReadAccess, 466 AllowedKeys: nil, 467 }, 468 }) 469 kp, err := wallet1.GenerateKeyPair(nil) 470 if err != nil { 471 t.Fatalf(err.Error()) 472 } 473 connectedWallet, err := api.NewConnectedWallet(hostname, wallet1) 474 if err != nil { 475 t.Fatalf(err.Error()) 476 } 477 478 // setup 479 handler := newCheckTransactionHandler(t) 480 // -- expected calls 481 handler.interactor.EXPECT().NotifyInteractionSessionBegan(ctx, traceID, api.TransactionReviewWorkflow, uint8(2)).Times(1).Return(nil) 482 handler.interactor.EXPECT().NotifyInteractionSessionEnded(ctx, gomock.Any()).Times(1) 483 handler.walletStore.EXPECT().GetWallet(ctx, wallet1.Name()).Times(1).Return(wallet1, nil) 484 handler.interactor.EXPECT().RequestTransactionReviewForChecking(ctx, traceID, uint8(1), hostname, wallet1.Name(), kp.PublicKey(), fakeTransaction, gomock.Any()).Times(1).Return(false, api.ErrUserCloseTheConnection) 485 handler.interactor.EXPECT().NotifyError(ctx, traceID, api.ApplicationErrorType, api.ErrConnectionClosed) 486 487 // when 488 result, errorDetails := handler.handle(t, ctx, api.ClientCheckTransactionParams{ 489 PublicKey: kp.PublicKey(), 490 Transaction: testTransaction(t), 491 }, connectedWallet) 492 493 // then 494 assertConnectionClosedError(t, errorDetails) 495 assert.Empty(t, result) 496 } 497 498 func testInterruptingTheRequestDoesNotCheckTransaction(t *testing.T) { 499 // given 500 ctx, traceID := clientContextForTest() 501 hostname := vgrand.RandomStr(5) 502 wallet1 := walletWithPerms(t, hostname, wallet.Permissions{ 503 PublicKeys: wallet.PublicKeysPermission{ 504 Access: wallet.ReadAccess, 505 AllowedKeys: nil, 506 }, 507 }) 508 kp, err := wallet1.GenerateKeyPair(nil) 509 if err != nil { 510 t.Fatalf(err.Error()) 511 } 512 connectedWallet, err := api.NewConnectedWallet(hostname, wallet1) 513 if err != nil { 514 t.Fatalf(err.Error()) 515 } 516 517 // setup 518 handler := newCheckTransactionHandler(t) 519 // -- expected calls 520 handler.interactor.EXPECT().NotifyInteractionSessionBegan(ctx, traceID, api.TransactionReviewWorkflow, uint8(2)).Times(1).Return(nil) 521 handler.interactor.EXPECT().NotifyInteractionSessionEnded(ctx, gomock.Any()).Times(1) 522 handler.walletStore.EXPECT().GetWallet(ctx, wallet1.Name()).Times(1).Return(wallet1, nil) 523 handler.interactor.EXPECT().RequestTransactionReviewForChecking(ctx, traceID, uint8(1), hostname, wallet1.Name(), kp.PublicKey(), fakeTransaction, gomock.Any()).Times(1).Return(false, api.ErrRequestInterrupted) 524 handler.interactor.EXPECT().NotifyError(ctx, traceID, api.ServerErrorType, api.ErrRequestInterrupted).Times(1) 525 526 // when 527 result, errorDetails := handler.handle(t, ctx, api.ClientCheckTransactionParams{ 528 PublicKey: kp.PublicKey(), 529 Transaction: testTransaction(t), 530 }, connectedWallet) 531 532 // then 533 assertRequestInterruptionError(t, errorDetails) 534 assert.Empty(t, result) 535 } 536 537 func testGettingInternalErrorDuringReviewDoesNotCheckTransaction(t *testing.T) { 538 // given 539 ctx, traceID := clientContextForTest() 540 hostname := vgrand.RandomStr(5) 541 wallet1 := walletWithPerms(t, hostname, wallet.Permissions{ 542 PublicKeys: wallet.PublicKeysPermission{ 543 Access: wallet.ReadAccess, 544 AllowedKeys: nil, 545 }, 546 }) 547 kp, err := wallet1.GenerateKeyPair(nil) 548 if err != nil { 549 t.Fatalf(err.Error()) 550 } 551 connectedWallet, err := api.NewConnectedWallet(hostname, wallet1) 552 if err != nil { 553 t.Fatalf(err.Error()) 554 } 555 556 // setup 557 handler := newCheckTransactionHandler(t) 558 // -- expected calls 559 handler.interactor.EXPECT().NotifyInteractionSessionBegan(ctx, traceID, api.TransactionReviewWorkflow, uint8(2)).Times(1).Return(nil) 560 handler.interactor.EXPECT().NotifyInteractionSessionEnded(ctx, gomock.Any()).Times(1) 561 handler.walletStore.EXPECT().GetWallet(ctx, wallet1.Name()).Times(1).Return(wallet1, nil) 562 handler.interactor.EXPECT().RequestTransactionReviewForChecking(ctx, traceID, uint8(1), hostname, wallet1.Name(), kp.PublicKey(), fakeTransaction, gomock.Any()).Times(1).Return(false, assert.AnError) 563 handler.interactor.EXPECT().NotifyError(ctx, traceID, api.InternalErrorType, fmt.Errorf("requesting the transaction review failed: %w", assert.AnError)).Times(1) 564 565 // when 566 result, errorDetails := handler.handle(t, ctx, api.ClientCheckTransactionParams{ 567 PublicKey: kp.PublicKey(), 568 Transaction: testTransaction(t), 569 }, connectedWallet) 570 571 // then 572 assertInternalError(t, errorDetails, api.ErrCouldNotCheckTransaction) 573 assert.Empty(t, result) 574 } 575 576 func testNoHealthyNodeAvailableDoesNotCheckTransaction(t *testing.T) { 577 // given 578 ctx, traceID := clientContextForTest() 579 hostname := vgrand.RandomStr(5) 580 wallet1 := walletWithPerms(t, hostname, wallet.Permissions{ 581 PublicKeys: wallet.PublicKeysPermission{ 582 Access: wallet.ReadAccess, 583 AllowedKeys: nil, 584 }, 585 }) 586 kp, err := wallet1.GenerateKeyPair(nil) 587 if err != nil { 588 t.Fatalf(err.Error()) 589 } 590 connectedWallet, err := api.NewConnectedWallet(hostname, wallet1) 591 if err != nil { 592 t.Fatalf(err.Error()) 593 } 594 595 // setup 596 handler := newCheckTransactionHandler(t) 597 // -- expected calls 598 handler.interactor.EXPECT().NotifyInteractionSessionBegan(ctx, traceID, api.TransactionReviewWorkflow, uint8(2)).Times(1).Return(nil) 599 handler.interactor.EXPECT().NotifyInteractionSessionEnded(ctx, gomock.Any()).Times(1) 600 handler.walletStore.EXPECT().GetWallet(ctx, wallet1.Name()).Times(1).Return(wallet1, nil) 601 handler.interactor.EXPECT().RequestTransactionReviewForChecking(ctx, traceID, uint8(1), hostname, wallet1.Name(), kp.PublicKey(), fakeTransaction, gomock.Any()).Times(1).Return(true, nil) 602 handler.nodeSelector.EXPECT().Node(ctx, gomock.Any()).Times(1).Return(nil, assert.AnError) 603 handler.interactor.EXPECT().NotifyError(ctx, traceID, api.NetworkErrorType, fmt.Errorf("could not find a healthy node: %w", assert.AnError)).Times(1) 604 handler.interactor.EXPECT().Log(ctx, traceID, gomock.Any(), gomock.Any()).AnyTimes() 605 606 // when 607 result, errorDetails := handler.handle(t, ctx, api.ClientCheckTransactionParams{ 608 PublicKey: kp.PublicKey(), 609 Transaction: testTransaction(t), 610 }, connectedWallet) 611 612 // then 613 require.NotNil(t, errorDetails) 614 assert.Equal(t, api.ErrorCodeNodeCommunicationFailed, errorDetails.Code) 615 assert.Equal(t, "Network error", errorDetails.Message) 616 assert.Equal(t, api.ErrNoHealthyNodeAvailable.Error(), errorDetails.Data) 617 assert.Empty(t, result) 618 } 619 620 func testFailingToGetSpamStatsDoesNotCheckTransaction(t *testing.T) { 621 // given 622 ctx, traceID := clientContextForTest() 623 hostname := vgrand.RandomStr(5) 624 wallet1 := walletWithPerms(t, hostname, wallet.Permissions{ 625 PublicKeys: wallet.PublicKeysPermission{ 626 Access: wallet.ReadAccess, 627 AllowedKeys: nil, 628 }, 629 }) 630 kp, err := wallet1.GenerateKeyPair(nil) 631 if err != nil { 632 t.Fatalf(err.Error()) 633 } 634 connectedWallet, err := api.NewConnectedWallet(hostname, wallet1) 635 if err != nil { 636 t.Fatalf(err.Error()) 637 } 638 639 // setup 640 handler := newCheckTransactionHandler(t) 641 // -- expected calls 642 handler.interactor.EXPECT().NotifyInteractionSessionBegan(ctx, traceID, api.TransactionReviewWorkflow, uint8(2)).Times(1).Return(nil) 643 handler.interactor.EXPECT().NotifyInteractionSessionEnded(ctx, gomock.Any()).Times(1) 644 handler.walletStore.EXPECT().GetWallet(ctx, wallet1.Name()).Times(1).Return(wallet1, nil) 645 handler.interactor.EXPECT().RequestTransactionReviewForChecking(ctx, traceID, uint8(1), hostname, wallet1.Name(), kp.PublicKey(), fakeTransaction, gomock.Any()).Times(1).Return(true, nil) 646 handler.nodeSelector.EXPECT().Node(ctx, gomock.Any()).Times(1).Return(handler.node, nil) 647 handler.node.EXPECT().SpamStatistics(ctx, kp.PublicKey()).Times(1).Return(types.SpamStatistics{}, assert.AnError) 648 handler.interactor.EXPECT().NotifyError(ctx, traceID, api.NetworkErrorType, fmt.Errorf("could not get the latest spam statistics for the public key from the node: %w", assert.AnError)).Times(1) 649 650 handler.interactor.EXPECT().Log(ctx, traceID, gomock.Any(), gomock.Any()).AnyTimes() 651 652 // when 653 result, errorDetails := handler.handle(t, ctx, api.ClientCheckTransactionParams{ 654 PublicKey: kp.PublicKey(), 655 Transaction: testTransaction(t), 656 }, connectedWallet) 657 658 // then 659 require.NotNil(t, errorDetails) 660 assert.Equal(t, api.ErrorCodeNodeCommunicationFailed, errorDetails.Code) 661 assert.Equal(t, "Network error", errorDetails.Message) 662 assert.Equal(t, api.ErrCouldNotGetSpamStatistics.Error(), errorDetails.Data) 663 assert.Empty(t, result) 664 } 665 666 func testFailureWhenCheckingTransactionReturnsAnError(t *testing.T) { 667 // given 668 ctx, traceID := clientContextForTest() 669 hostname := vgrand.RandomStr(5) 670 nodeHost := vgrand.RandomStr(5) 671 wallet1 := walletWithPerms(t, hostname, wallet.Permissions{ 672 PublicKeys: wallet.PublicKeysPermission{ 673 Access: wallet.ReadAccess, 674 AllowedKeys: nil, 675 }, 676 }) 677 kp, err := wallet1.GenerateKeyPair(nil) 678 if err != nil { 679 t.Fatalf(err.Error()) 680 } 681 connectedWallet, err := api.NewConnectedWallet(hostname, wallet1) 682 if err != nil { 683 t.Fatalf(err.Error()) 684 } 685 stats := types.SpamStatistics{ 686 ChainID: vgrand.RandomStr(5), 687 LastBlockHeight: 100, 688 } 689 690 // setup 691 handler := newCheckTransactionHandler(t) 692 // -- expected calls 693 handler.interactor.EXPECT().NotifyInteractionSessionBegan(ctx, traceID, api.TransactionReviewWorkflow, uint8(2)).Times(1).Return(nil) 694 handler.interactor.EXPECT().NotifyInteractionSessionEnded(ctx, gomock.Any()).Times(1) 695 handler.walletStore.EXPECT().GetWallet(ctx, wallet1.Name()).Times(1).Return(wallet1, nil) 696 handler.interactor.EXPECT().RequestTransactionReviewForChecking(ctx, traceID, uint8(1), hostname, wallet1.Name(), kp.PublicKey(), fakeTransaction, gomock.Any()).Times(1).Return(true, nil) 697 handler.nodeSelector.EXPECT().Node(ctx, gomock.Any()).Times(1).Return(handler.node, nil) 698 handler.node.EXPECT().SpamStatistics(ctx, kp.PublicKey()).Times(1).Return(stats, nil) 699 handler.node.EXPECT().Host().Times(1).Return(nodeHost) 700 handler.spam.EXPECT().CheckSubmission(gomock.Any(), &stats).Times(1) 701 handler.spam.EXPECT().GenerateProofOfWork(kp.PublicKey(), &stats).Times(1).Return(&commandspb.ProofOfWork{ 702 Tid: vgrand.RandomStr(5), 703 Nonce: 12345678, 704 }, nil) 705 handler.node.EXPECT().CheckTransaction(ctx, gomock.Any()).Times(1).Return(assert.AnError) 706 handler.interactor.EXPECT().NotifyFailedTransaction(ctx, traceID, uint8(2), gomock.Any(), gomock.Any(), assert.AnError, gomock.Any(), nodeHost).Times(1) 707 handler.interactor.EXPECT().Log(ctx, traceID, gomock.Any(), gomock.Any()).AnyTimes() 708 709 // when 710 result, errorDetails := handler.handle(t, ctx, api.ClientCheckTransactionParams{ 711 PublicKey: kp.PublicKey(), 712 Transaction: testTransaction(t), 713 }, connectedWallet) 714 715 // then 716 require.NotNil(t, errorDetails) 717 assert.Equal(t, api.ErrorCodeNodeCommunicationFailed, errorDetails.Code) 718 assert.Equal(t, "Network error", errorDetails.Message) 719 assert.Equal(t, "the transaction failed: assert.AnError general error for testing", errorDetails.Data) 720 assert.Empty(t, result) 721 } 722 723 func testFailingSpamChecksAbortsCheckingTheTransaction(t *testing.T) { 724 // given 725 ctx, traceID := clientContextForTest() 726 hostname := vgrand.RandomStr(5) 727 wallet1 := walletWithPerms(t, hostname, wallet.Permissions{ 728 PublicKeys: wallet.PublicKeysPermission{ 729 Access: wallet.ReadAccess, 730 AllowedKeys: nil, 731 }, 732 }) 733 kp, err := wallet1.GenerateKeyPair(nil) 734 if err != nil { 735 t.Fatalf(err.Error()) 736 } 737 connectedWallet, err := api.NewConnectedWallet(hostname, wallet1) 738 if err != nil { 739 t.Fatalf(err.Error()) 740 } 741 spamStats := types.SpamStatistics{ 742 ChainID: vgrand.RandomStr(5), 743 LastBlockHeight: 100, 744 Proposals: &types.SpamStatistic{MaxForEpoch: 1}, 745 NodeAnnouncements: &types.SpamStatistic{MaxForEpoch: 1}, 746 Delegations: &types.SpamStatistic{MaxForEpoch: 1}, 747 Transfers: &types.SpamStatistic{MaxForEpoch: 1}, 748 Votes: &types.VoteSpamStatistics{MaxForEpoch: 1}, 749 PoW: &types.PoWStatistics{ 750 PowBlockStates: []types.PoWBlockState{{}}, 751 }, 752 } 753 754 // setup 755 handler := newCheckTransactionHandler(t) 756 757 // -- expected calls 758 handler.interactor.EXPECT().NotifyInteractionSessionBegan(ctx, traceID, api.TransactionReviewWorkflow, uint8(2)).Times(1).Return(nil) 759 handler.interactor.EXPECT().NotifyInteractionSessionEnded(ctx, traceID).Times(1) 760 handler.walletStore.EXPECT().GetWallet(ctx, wallet1.Name()).Times(1).Return(wallet1, nil) 761 handler.interactor.EXPECT().RequestTransactionReviewForChecking(ctx, traceID, uint8(1), hostname, wallet1.Name(), kp.PublicKey(), fakeTransaction, gomock.Any()).Times(1).Return(true, nil) 762 handler.nodeSelector.EXPECT().Node(ctx, gomock.Any()).Times(1).Return(handler.node, nil) 763 handler.node.EXPECT().SpamStatistics(ctx, kp.PublicKey()).Times(1).Return(spamStats, nil) 764 handler.spam.EXPECT().CheckSubmission(gomock.Any(), &spamStats).Times(1).Return(assert.AnError) 765 handler.interactor.EXPECT().NotifyError(ctx, traceID, api.ApplicationErrorType, gomock.Any()).Times(1) 766 handler.interactor.EXPECT().Log(ctx, traceID, gomock.Any(), gomock.Any()).AnyTimes() 767 768 // when 769 result, errorDetails := handler.handle(t, ctx, api.ClientCheckTransactionParams{ 770 PublicKey: kp.PublicKey(), 771 Transaction: testTransaction(t), 772 }, connectedWallet) 773 774 // then 775 require.NotNil(t, errorDetails) 776 assert.Equal(t, api.ErrorCodeRequestHasBeenCancelledByApplication, errorDetails.Code) 777 assert.Equal(t, "Application error", errorDetails.Message) 778 assert.Equal(t, assert.AnError.Error(), errorDetails.Data) 779 assert.Empty(t, result) 780 } 781 782 type checkTransactionHandler struct { 783 *api.ClientCheckTransaction 784 ctrl *gomock.Controller 785 interactor *mocks.MockInteractor 786 nodeSelector *nodemocks.MockSelector 787 node *nodemocks.MockNode 788 walletStore *mocks.MockWalletStore 789 spam *mocks.MockSpamHandler 790 } 791 792 func (h *checkTransactionHandler) handle(t *testing.T, ctx context.Context, params jsonrpc.Params, connectedWallet api.ConnectedWallet) (api.ClientCheckTransactionResult, *jsonrpc.ErrorDetails) { 793 t.Helper() 794 795 rawResult, err := h.Handle(ctx, params, connectedWallet) 796 if rawResult != nil { 797 result, ok := rawResult.(api.ClientCheckTransactionResult) 798 if !ok { 799 t.Fatal("ClientSendTransaction handler result is not a ClientCheckTransactionResult") 800 } 801 return result, err 802 } 803 return api.ClientCheckTransactionResult{}, err 804 } 805 806 func newCheckTransactionHandler(t *testing.T) *checkTransactionHandler { 807 t.Helper() 808 809 ctrl := gomock.NewController(t) 810 nodeSelector := nodemocks.NewMockSelector(ctrl) 811 interactor := mocks.NewMockInteractor(ctrl) 812 proofOfWork := mocks.NewMockSpamHandler(ctrl) 813 walletStore := mocks.NewMockWalletStore(ctrl) 814 node := nodemocks.NewMockNode(ctrl) 815 816 requestController := api.NewRequestController( 817 api.WithMaximumAttempt(1), 818 api.WithIntervalDelayBetweenRetries(1*time.Second), 819 ) 820 821 return &checkTransactionHandler{ 822 ClientCheckTransaction: api.NewClientCheckTransaction(walletStore, interactor, nodeSelector, proofOfWork, requestController), 823 ctrl: ctrl, 824 nodeSelector: nodeSelector, 825 interactor: interactor, 826 node: node, 827 walletStore: walletStore, 828 spam: proofOfWork, 829 } 830 }