From 3ee4fe0d27e9e34ca8b66ea904337862bbb68ca5 Mon Sep 17 00:00:00 2001 From: Dzmitry Hil Date: Fri, 5 Apr 2024 16:24:40 +0300 Subject: [PATCH] Add admin actions stress test (#199) * Refactor stress tests to make them more maintainable * Add admin actions stress test --- integration-tests/bridge.go | 7 + integration-tests/init.go | 18 +- integration-tests/stress/env_test.go | 357 ++++++++++++++ .../stress/sending_stress_test.go | 438 ------------------ integration-tests/stress/stress_test.go | 347 ++++++++++++++ relayer/client/bridge.go | 8 + relayer/xrpl/rpc.go | 25 + 7 files changed, 752 insertions(+), 448 deletions(-) create mode 100644 integration-tests/bridge.go create mode 100644 integration-tests/stress/env_test.go delete mode 100644 integration-tests/stress/sending_stress_test.go create mode 100644 integration-tests/stress/stress_test.go diff --git a/integration-tests/bridge.go b/integration-tests/bridge.go new file mode 100644 index 00000000..021b87b6 --- /dev/null +++ b/integration-tests/bridge.go @@ -0,0 +1,7 @@ +package integrationtests + +// BridgeConfig the deployed bridge config. +type BridgeConfig struct { + ContractAddress string + OwnerMnemonic string +} diff --git a/integration-tests/init.go b/integration-tests/init.go index 89f605b5..0f9c4f19 100644 --- a/integration-tests/init.go +++ b/integration-tests/init.go @@ -9,7 +9,6 @@ import ( "testing" "time" - sdk "github.com/cosmos/cosmos-sdk/types" "github.com/pkg/errors" "github.com/stretchr/testify/require" @@ -20,9 +19,9 @@ var chains Chains // flag variables. var ( - coreumCfg CoreumChainConfig - xrplCfg XRPLChainConfig - contractAddressString string + coreumCfg CoreumChainConfig + xrplCfg XRPLChainConfig + bridgeCfg BridgeConfig ) // Chains struct holds chains required for the testing. @@ -39,7 +38,8 @@ func init() { flag.StringVar(&xrplCfg.RPCAddress, "xrpl-rpc-address", "http://localhost:5005", "RPC address of xrpl node") flag.StringVar(&xrplCfg.FundingSeed, "xrpl-funding-seed", "snoPBrXtMeMyMHUVTgbuqAfg1SUTb", "Funding XRPL account seed required by tests") // this is the default address used in znet - flag.StringVar(&contractAddressString, "contract-address", "devcore14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9sd4f0ak", "Smart contract address of the bridge") + flag.StringVar(&bridgeCfg.ContractAddress, "contract-address", "devcore14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9sd4f0ak", "Smart contract address of the bridge (znet)") + flag.StringVar(&bridgeCfg.OwnerMnemonic, "owner-mnemonic", "analyst evil lucky job exhaust inform note where grant file already exit vibrant come finger spatial absorb enter aisle orange soldier false attend response", "Smart contract owner of the bridge (znet)") // accept testing flags testing.Init() @@ -79,9 +79,7 @@ func NewTestingContext(t *testing.T) (context.Context, Chains) { return testCtx, chains } -// GetContractAddress returns the contract address for the bridge. -func GetContractAddress(t *testing.T) sdk.AccAddress { - address, err := sdk.AccAddressFromBech32(contractAddressString) - require.NoError(t, err) - return address +// GetBridgeConfig returns the bridge config. +func GetBridgeConfig() BridgeConfig { + return bridgeCfg } diff --git a/integration-tests/stress/env_test.go b/integration-tests/stress/env_test.go new file mode 100644 index 00000000..cd09f12a --- /dev/null +++ b/integration-tests/stress/env_test.go @@ -0,0 +1,357 @@ +//go:build integrationtests +// +build integrationtests + +package stress_test + +import ( + "context" + "testing" + "time" + + sdkmath "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + "github.com/pkg/errors" + rippledata "github.com/rubblelabs/ripple/data" + "github.com/stretchr/testify/require" + + "github.com/CoreumFoundation/coreum-tools/pkg/parallel" + "github.com/CoreumFoundation/coreum-tools/pkg/retry" + "github.com/CoreumFoundation/coreum/v4/pkg/client" + coreumintegration "github.com/CoreumFoundation/coreum/v4/testutil/integration" + integrationtests "github.com/CoreumFoundation/coreumbridge-xrpl/integration-tests" + bridgeclient "github.com/CoreumFoundation/coreumbridge-xrpl/relayer/client" + "github.com/CoreumFoundation/coreumbridge-xrpl/relayer/coreum" + "github.com/CoreumFoundation/coreumbridge-xrpl/relayer/xrpl" +) + +// EnvConfig is stress test env config. +type EnvConfig struct { + TestTimeout time.Duration + TestCaseTimeout time.Duration + AwaitStateTimeout time.Duration + ParallelExecutionNumber int + RepeatTestCaseCount int + RepeatOwnerActionCount int + OwnerActionDelay time.Duration +} + +// DefaultEnvConfig returns default env config. +func DefaultEnvConfig() EnvConfig { + return EnvConfig{ + TestTimeout: time.Hour, + TestCaseTimeout: 5 * time.Minute, + AwaitStateTimeout: time.Second, + ParallelExecutionNumber: 3, + RepeatTestCaseCount: 5, + RepeatOwnerActionCount: 10, + OwnerActionDelay: 5 * time.Second, + } +} + +// Env is stress test env. +type Env struct { + Cfg EnvConfig + Chains integrationtests.Chains + ContractOwner sdk.AccAddress + BridgeClient *bridgeclient.BridgeClient + ContractClient *coreum.ContractClient +} + +// NewEnv returns new instance of Env. +func NewEnv(t *testing.T, cfg EnvConfig) *Env { + _, chains := integrationtests.NewTestingContext(t) + bridgeCfg := integrationtests.GetBridgeConfig() + + contractClient := coreum.NewContractClient( + coreum.DefaultContractClientConfig(sdk.MustAccAddressFromBech32(bridgeCfg.ContractAddress)), + chains.Log, + chains.Coreum.ClientContext, + ) + + bridgeClient := bridgeclient.NewBridgeClient( + chains.Log, + chains.Coreum.ClientContext, + contractClient, + chains.XRPL.RPCClient(), + xrpl.NewKeyringTxSigner(chains.XRPL.GetSignerKeyring()), + ) + + // import contract owner mnemonic + contractOwner := chains.Coreum.ChainContext.ImportMnemonic(bridgeCfg.OwnerMnemonic) + + return &Env{ + Cfg: cfg, + Chains: chains, + ContractOwner: contractOwner, + BridgeClient: bridgeClient, + ContractClient: contractClient, + } +} + +// NewBridgeClient returns new instance of BridgeClient. +func (env *Env) NewBridgeClient() *bridgeclient.BridgeClient { + bridgeCfg := integrationtests.GetBridgeConfig() + contractClient := coreum.NewContractClient( + coreum.DefaultContractClientConfig(sdk.MustAccAddressFromBech32(bridgeCfg.ContractAddress)), + env.Chains.Log, + env.Chains.Coreum.ClientContext, + ) + + return bridgeclient.NewBridgeClient( + env.Chains.Log, + env.Chains.Coreum.ClientContext, + contractClient, + env.Chains.XRPL.RPCClient(), + xrpl.NewKeyringTxSigner(env.Chains.XRPL.GetSignerKeyring()), + ) +} + +// FundCoreumAccountsWithXRP funds the Coreum accounts with the particular XRP token thought the bridge on the +// Coreum chain. +func (env *Env) FundCoreumAccountsWithXRP( + ctx context.Context, + t *testing.T, + coreumAccounts []sdk.AccAddress, + amount sdkmath.Int, +) { + totalXRPLValueToSend, err := rippledata.NewNativeValue(int64(amount.Uint64() * uint64(len(coreumAccounts)))) + require.NoError(t, err) + + xrplFaucetAccount := env.Chains.XRPL.GenAccount(ctx, t, totalXRPLValueToSend.Float()) + + registeredXRPToken, err := env.ContractClient.GetXRPLTokenByIssuerAndCurrency( + ctx, xrpl.XRPTokenIssuer.String(), xrpl.ConvertCurrencyToString(xrpl.XRPTokenCurrency), + ) + require.NoError(t, err) + + xrpAmount := rippledata.Amount{ + Value: totalXRPLValueToSend, + Currency: xrpl.XRPTokenCurrency, + Issuer: xrpl.XRPTokenIssuer, + } + + coreumFaucetAccount := env.Chains.Coreum.GenAccount() + + require.NoError( + t, env.BridgeClient.SendFromXRPLToCoreum(ctx, xrplFaucetAccount.String(), xrpAmount, coreumFaucetAccount), + ) + + require.NoError(t, env.AwaitCoreumBalance( + ctx, + coreumFaucetAccount, + sdk.NewCoin( + registeredXRPToken.CoreumDenom, + integrationtests.ConvertStringWithDecimalsToSDKInt( + t, + totalXRPLValueToSend.String(), + xrpl.XRPCurrencyDecimals, + )), + )) + + msg := &banktypes.MsgMultiSend{ + Inputs: []banktypes.Input{{ + Address: coreumFaucetAccount.String(), + Coins: sdk.NewCoins(sdk.NewCoin(registeredXRPToken.CoreumDenom, amount.MulRaw(int64(len(coreumAccounts))))), + }}, + Outputs: []banktypes.Output{}, + } + for _, acc := range coreumAccounts { + acc := acc + msg.Outputs = append(msg.Outputs, banktypes.Output{ + Address: acc.String(), + Coins: sdk.NewCoins(sdk.NewCoin(registeredXRPToken.CoreumDenom, amount)), + }) + } + env.Chains.Coreum.FundAccountWithOptions(ctx, t, coreumFaucetAccount, coreumintegration.BalancesOptions{ + Messages: []sdk.Msg{msg}, + }) + + _, err = client.BroadcastTx( + ctx, + env.Chains.Coreum.ClientContext.WithFromAddress(coreumFaucetAccount), + env.Chains.Coreum.TxFactory().WithSimulateAndExecute(true), + msg, + ) + require.NoError(t, err) +} + +// GenCoreumAndXRPLAccounts generates Coreum and XRPL accounts. +func (env *Env) GenCoreumAndXRPLAccounts( + ctx context.Context, + t *testing.T, + coreumAccountCount int, + coreumAccountAmount sdkmath.Int, + xrplAccountCount int, + xrplAccountAmount float64, +) ([]sdk.AccAddress, []rippledata.Account) { + var ( + coreumAccounts []sdk.AccAddress + xrplAccounts []rippledata.Account + ) + require.NoError(t, parallel.Run(ctx, func(ctx context.Context, spawn parallel.SpawnFn) error { + spawn("gen-coreum-accounts", parallel.Continue, func(ctx context.Context) error { + coreumAccounts = env.GenCoreumAccounts(ctx, t, coreumAccountCount, coreumAccountAmount) + return nil + }) + spawn("gen-xrpl-accounts", parallel.Continue, func(ctx context.Context) error { + xrplAccounts = env.GenXRPLAccounts(ctx, t, xrplAccountCount, xrplAccountAmount) + return nil + }) + return nil + })) + + return coreumAccounts, xrplAccounts +} + +// GenCoreumAccounts generates coreum accounts with the provided amount. +func (env *Env) GenCoreumAccounts(ctx context.Context, t *testing.T, count int, amount sdkmath.Int) []sdk.AccAddress { + coreumAccounts := make([]sdk.AccAddress, 0, count) + for i := 0; i < count; i++ { + acc := env.Chains.Coreum.GenAccount() + env.Chains.Coreum.FundAccountWithOptions(ctx, t, acc, coreumintegration.BalancesOptions{ + Amount: amount, + }) + coreumAccounts = append(coreumAccounts, acc) + } + + return coreumAccounts +} + +// GenXRPLAccounts generates XRPL accounts. +func (env *Env) GenXRPLAccounts(ctx context.Context, t *testing.T, count int, amount float64) []rippledata.Account { + xrplAccounts := make([]rippledata.Account, 0, count) + for i := 0; i < count; i++ { + acc := env.Chains.XRPL.GenAccount(ctx, t, amount) + xrplAccounts = append(xrplAccounts, acc) + } + + return xrplAccounts +} + +// AwaitCoreumBalance waits for expected coreum balance. +func (env *Env) AwaitCoreumBalance( + ctx context.Context, + address sdk.AccAddress, + expectedBalance sdk.Coin, +) error { + bankClient := banktypes.NewQueryClient(env.Chains.Coreum.ClientContext) + return env.AwaitState(ctx, func() error { + balancesRes, err := bankClient.AllBalances(ctx, &banktypes.QueryAllBalancesRequest{ + Address: address.String(), + }) + if err != nil { + return err + } + + if balancesRes.Balances.AmountOf(expectedBalance.Denom).String() != expectedBalance.Amount.String() { + return retry.Retryable(errors.Errorf( + "balance of %s is not as expected, all balances: %s", + expectedBalance.String(), + balancesRes.Balances.String()), + ) + } + + return nil + }) +} + +// AwaitXRPLBalance awaits for the balance on the XRPL change. +func (env *Env) AwaitXRPLBalance( + ctx context.Context, + account rippledata.Account, + amount rippledata.Amount, +) error { + return env.AwaitState(ctx, func() error { + balances, err := env.Chains.XRPL.RPCClient().GetXRPLBalances(ctx, account) + if err != nil { + return err + } + for _, balance := range balances { + if balance.String() == amount.String() { + return nil + } + } + return errors.Errorf("balance is not euqal to expected (%+v), all balances:%v", amount.String(), balances) + }) +} + +// AwaitPendingRefund awaits for pending refunds on the Coreum change. +func (env *Env) AwaitPendingRefund( + ctx context.Context, + account sdk.AccAddress, +) ([]coreum.PendingRefund, error) { + var refunds []coreum.PendingRefund + if err := env.AwaitState(ctx, func() error { + var err error + refunds, err = env.ContractClient.GetPendingRefunds(ctx, account) + if err != nil { + return err + } + if len(refunds) == 0 { + return errors.Errorf("no pending refunds for address %s", account.String()) + } + return nil + }); err != nil { + return nil, err + } + return refunds, nil +} + +// AwaitState awaits for particular state. +func (env *Env) AwaitState(ctx context.Context, stateChecker func() error) error { + return retry.Do(ctx, env.Cfg.AwaitStateTimeout, func() error { + if err := stateChecker(); err != nil { + return retry.Retryable(err) + } + + return nil + }) +} + +// RepeatOwnerActionWithDelay calls the action, waits for some time and call the actionCompensation. +func (env *Env) RepeatOwnerActionWithDelay(ctx context.Context, action, rollbackAction func() error) error { + for j := 0; j < env.Cfg.RepeatOwnerActionCount; j++ { + if err := env.callAdminAction(ctx, action, rollbackAction); err != nil { + return err + } + } + return nil +} + +// AwaitContractCall awaits for the call to the contract to be executed if the error is expected. +func (env *Env) AwaitContractCall(ctx context.Context, call func() error) error { + return retry.Do(ctx, env.Cfg.AwaitStateTimeout, func() error { + if err := call(); err != nil { + if coreum.IsTokenNotEnabledError(err) || + coreum.IsBridgeHaltedError(err) || + coreum.IsNoAvailableTicketsError(err) || + coreum.IsLastTicketReservedError(err) { + return retry.Retryable(err) + } + return err + } + + return nil + }) +} + +func (env *Env) callAdminAction(ctx context.Context, action func() error, rollbackAction func() error) error { + ctx, cancel := context.WithTimeout(ctx, env.Cfg.TestCaseTimeout) + defer cancel() + // use common BridgeClient to prevent sequence mismatch + if err := env.AwaitContractCall(ctx, func() error { + return action() + }); err != nil { + return err + } + select { + case <-ctx.Done(): + return ctx.Err() + case <-time.After(env.Cfg.OwnerActionDelay): + } + // use common BridgeClient to prevent sequence mismatch + return env.AwaitContractCall(ctx, func() error { + return rollbackAction() + }) +} diff --git a/integration-tests/stress/sending_stress_test.go b/integration-tests/stress/sending_stress_test.go deleted file mode 100644 index ebb38dd2..00000000 --- a/integration-tests/stress/sending_stress_test.go +++ /dev/null @@ -1,438 +0,0 @@ -//go:build integrationtests -// +build integrationtests - -package stress_test - -import ( - "context" - "math" - "strconv" - "testing" - "time" - - sdkmath "cosmossdk.io/math" - sdk "github.com/cosmos/cosmos-sdk/types" - banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" - "github.com/pkg/errors" - rippledata "github.com/rubblelabs/ripple/data" - "github.com/stretchr/testify/require" - - "github.com/CoreumFoundation/coreum-tools/pkg/parallel" - "github.com/CoreumFoundation/coreum-tools/pkg/retry" - "github.com/CoreumFoundation/coreum/v4/pkg/client" - coreumintegration "github.com/CoreumFoundation/coreum/v4/testutil/integration" - integrationtests "github.com/CoreumFoundation/coreumbridge-xrpl/integration-tests" - bridgeclient "github.com/CoreumFoundation/coreumbridge-xrpl/relayer/client" - "github.com/CoreumFoundation/coreumbridge-xrpl/relayer/coreum" - "github.com/CoreumFoundation/coreumbridge-xrpl/relayer/xrpl" -) - -var ( - testAccounts = 2 - iterationPerAccount = 30 -) - -func TestStressSendFromXRPLToCoreumAndBack(t *testing.T) { - _, chains := integrationtests.NewTestingContext(t) - testCount := testAccounts * iterationPerAccount - sendAmount := 1 - ctx, cancel := context.WithTimeout(context.Background(), time.Minute*time.Duration(testCount)+5*time.Minute) - t.Cleanup(cancel) - - contractClient := coreum.NewContractClient( - coreum.DefaultContractClientConfig(integrationtests.GetContractAddress(t)), - chains.Log, - chains.Coreum.ClientContext, - ) - - xrplTxSigner := xrpl.NewKeyringTxSigner(chains.XRPL.GetSignerKeyring()) - bridgeClient := bridgeclient.NewBridgeClient( - chains.Log, - chains.Coreum.ClientContext, - contractClient, - chains.XRPL.RPCClient(), - xrplTxSigner, - ) - - // setup amount to send - registeredXRPToken, err := contractClient.GetXRPLTokenByIssuerAndCurrency( - ctx, xrpl.XRPTokenIssuer.String(), xrpl.ConvertCurrencyToString(xrpl.XRPTokenCurrency), - ) - require.NoError(t, err) - - valueToSendFromXRPLtoCoreum, err := rippledata.NewNativeValue(int64(sendAmount)) - require.NoError(t, err) - amountToSendFromXRPLtoCoreum := rippledata.Amount{ - Value: valueToSendFromXRPLtoCoreum, - Currency: xrpl.XRPTokenCurrency, - Issuer: xrpl.XRPTokenIssuer, - } - - // generate and fund accounts - xrplRecipientAddress := chains.XRPL.GenAccount(ctx, t, 0) - xrplRecipientBalanceBefore := chains.XRPL.GetAccountBalance( - ctx, t, xrplRecipientAddress, xrpl.XRPTokenIssuer, xrpl.XRPTokenCurrency, - ) - coreumAccounts := make([]sdk.AccAddress, 0) - xrplAccounts := make([]rippledata.Account, 0) - - t.Log("Generating and funding accounts") - for i := 0; i < testAccounts; i++ { - newCoreumAccount := chains.Coreum.GenAccount() - coreumAccounts = append(coreumAccounts, newCoreumAccount) - chains.Coreum.FundAccountWithOptions(ctx, t, newCoreumAccount, coreumintegration.BalancesOptions{ - Amount: sdkmath.NewIntFromUint64(500_000 * uint64(iterationPerAccount)), - }) - newXRPLAccount := chains.XRPL.GenAccount(ctx, t, 0.1*float64(iterationPerAccount)) - xrplAccounts = append(xrplAccounts, newXRPLAccount) - } - - t.Log("Accounts generated and funded") - - startTime := time.Now() - err = parallel.Run(ctx, func(ctx context.Context, spawn parallel.SpawnFn) error { - for i := 0; i < testAccounts; i++ { - coreumAccount := coreumAccounts[i] - xrplAccount := xrplAccounts[i] - spawn(strconv.Itoa(i), parallel.Continue, func(ctx context.Context) error { - for j := 0; j < iterationPerAccount; j++ { - err := bridgeClient.SendFromXRPLToCoreum( - ctx, xrplAccount.String(), amountToSendFromXRPLtoCoreum, coreumAccount, - ) - if err != nil { - return err - } - err = chains.Coreum.AwaitForBalance( - ctx, - t, - coreumAccount, - sdk.NewCoin( - registeredXRPToken.CoreumDenom, - integrationtests.ConvertStringWithDecimalsToSDKInt( - t, - valueToSendFromXRPLtoCoreum.String(), - xrpl.XRPCurrencyDecimals, - )), - ) - if err != nil { - return err - } - - // send back to xrpl right after it is received on coreum - err = bridgeClient.SendFromCoreumToXRPL( - ctx, - coreumAccount, - xrplRecipientAddress, - sdk.NewCoin( - registeredXRPToken.CoreumDenom, - integrationtests.ConvertStringWithDecimalsToSDKInt( - t, - valueToSendFromXRPLtoCoreum.String(), - xrpl.XRPCurrencyDecimals, - )), - nil, - ) - if err != nil { - return err - } - } - return nil - }) - } - return nil - }) - require.NoError(t, err) - - expectedReceived, err := rippledata.NewNativeValue(int64(sendAmount * testCount)) - require.NoError(t, err) - expectedCurrentBalance, err := expectedReceived.Add(*xrplRecipientBalanceBefore.Value) - require.NoError(t, err) - - awaitXRPLBalance( - ctx, - t, - chains.XRPL, - xrplRecipientAddress, - xrpl.XRPTokenIssuer, - xrpl.XRPTokenCurrency, - *expectedCurrentBalance, - ) - testDuration := time.Since(startTime) - - xrplRecipientBalanceAfter := chains.XRPL.GetAccountBalance( - ctx, t, xrplRecipientAddress, xrpl.XRPTokenIssuer, xrpl.XRPTokenCurrency, - ) - received, err := xrplRecipientBalanceAfter.Value.Subtract(*xrplRecipientBalanceBefore.Value) - require.NoError(t, err) - require.Equal(t, expectedReceived.String(), received.String()) - - t.Logf("Ran %d Operations in %s, %s per operation", testCount, testDuration, testDuration/time.Duration(testCount)) -} - -func TestStressSendWithFailureAndClaimRefund(t *testing.T) { - _, chains := integrationtests.NewTestingContext(t) - testCount := testAccounts * iterationPerAccount - ctx, cancel := context.WithTimeout(context.Background(), time.Minute*time.Duration(testCount)) - t.Cleanup(cancel) - - contractClient := coreum.NewContractClient( - coreum.DefaultContractClientConfig(integrationtests.GetContractAddress(t)), - chains.Log, - chains.Coreum.ClientContext, - ) - bankClient := banktypes.NewQueryClient(chains.Coreum.ClientContext) - - xrplTxSigner := xrpl.NewKeyringTxSigner(chains.XRPL.GetSignerKeyring()) - bridgeClient := bridgeclient.NewBridgeClient( - chains.Log, - chains.Coreum.ClientContext, - contractClient, - chains.XRPL.RPCClient(), - xrplTxSigner, - ) - - sendAmount := 1 - valueToSendFromXRPLtoCoreum, err := rippledata.NewNativeValue(int64(sendAmount)) - require.NoError(t, err) - - registeredXRPToken, err := contractClient.GetXRPLTokenByIssuerAndCurrency( - ctx, xrpl.XRPTokenIssuer.String(), xrpl.ConvertCurrencyToString(xrpl.XRPTokenCurrency), - ) - require.NoError(t, err) - // generate and fund accounts - type xrplAccount struct { - Account rippledata.Account - Exists bool - } - xrplAccounts := make([]xrplAccount, 0) - coreumAccounts := make([]sdk.AccAddress, 0) - - t.Log("Generating and funding accounts") - for i := 0; i < testAccounts; i++ { - newCoreumAccount := chains.Coreum.GenAccount() - coreumAccounts = append(coreumAccounts, newCoreumAccount) - chains.Coreum.FundAccountWithOptions(ctx, t, newCoreumAccount, coreumintegration.BalancesOptions{ - Amount: sdkmath.NewIntFromUint64(500_000 * uint64(iterationPerAccount)), - }) - var newXRPLAccount xrplAccount - // every 1 in 5 accounts should be empty to simulate failure. - if i%5 == 0 { - newXRPLAccount = xrplAccount{chains.XRPL.GenEmptyAccount(t), false} - } else { - newXRPLAccount = xrplAccount{chains.XRPL.GenAccount(ctx, t, 0), true} - } - xrplAccounts = append(xrplAccounts, newXRPLAccount) - } - - fundCoreumAccountsWithXRP( - ctx, - t, - chains, - *bridgeClient, - registeredXRPToken.CoreumDenom, - coreumAccounts, - xrpValueMulRaw(t, valueToSendFromXRPLtoCoreum, int64(iterationPerAccount)), - ) - - t.Log("Accounts generated and funded") - - err = parallel.Run(ctx, func(ctx context.Context, spawn parallel.SpawnFn) error { - for i := 0; i < testAccounts; i++ { - coreumAccount := coreumAccounts[i] - xrplAccount := xrplAccounts[i] - // accounts start with 10 initial xrp balance. - expectedBalance, err := rippledata.NewValue("10000000", true) - require.NoError(t, err) - spawn(strconv.Itoa(i), parallel.Continue, func(ctx context.Context) error { - for j := 0; j < iterationPerAccount; j++ { - err = bridgeClient.SendFromCoreumToXRPL( - ctx, - coreumAccount, - xrplAccount.Account, - sdk.NewCoin( - registeredXRPToken.CoreumDenom, - integrationtests.ConvertStringWithDecimalsToSDKInt( - t, - valueToSendFromXRPLtoCoreum.String(), - xrpl.XRPCurrencyDecimals, - )), - nil, - ) - if err != nil { - return err - } - - if xrplAccount.Exists { - expectedBalance, err = expectedBalance.Add(*valueToSendFromXRPLtoCoreum) - if err != nil { - return err - } - awaitXRPLBalance( - ctx, - t, - chains.XRPL, - xrplAccount.Account, - xrpl.XRPTokenIssuer, - xrpl.XRPTokenCurrency, - *expectedBalance, - ) - } else { - refunds := awaitPendingRefund(ctx, t, contractClient, coreumAccount) - require.Len(t, refunds, 1) - balanceBefore, err := bankClient.Balance( - ctx, banktypes.NewQueryBalanceRequest(coreumAccount, registeredXRPToken.CoreumDenom), - ) - require.NoError(t, err) - _, err = contractClient.ClaimRefund(ctx, coreumAccount, refunds[0].ID) - require.NoError(t, err) - balanceAfter, err := bankClient.Balance( - ctx, banktypes.NewQueryBalanceRequest(coreumAccount, registeredXRPToken.CoreumDenom), - ) - require.NoError(t, err) - balanceChange := balanceAfter.GetBalance().Amount.Sub(balanceBefore.Balance.Amount) - require.EqualValues(t, balanceChange.Int64(), sendAmount) - } - } - return nil - }) - } - return nil - }) - require.NoError(t, err) -} - -func xrpValueMulRaw(t *testing.T, rValue *rippledata.Value, n int64) *rippledata.Value { - var nValue *rippledata.Value - var err error - if rValue.IsNative() { - nValue, err = rippledata.NewNativeValue(n) - require.NoError(t, err) - } else { - nValue, err = rippledata.NewNonNativeValue(n, 0) - require.NoError(t, err) - } - res, err := rValue.Multiply(*nValue) - require.NoError(t, err) - return res -} - -func fundCoreumAccountsWithXRP( - ctx context.Context, - t *testing.T, - chains integrationtests.Chains, - bridgeClient bridgeclient.BridgeClient, - registeredXrpDenomOnCoreum string, - coreumAccounts []sdk.AccAddress, - xrpToEachAccount *rippledata.Value, -) { - coreumAccount := chains.Coreum.GenAccount() - totalSendValue := xrpValueMulRaw(t, xrpToEachAccount, int64(len(coreumAccounts))) - xrplAccount := chains.XRPL.GenAccount(ctx, t, totalSendValue.Float()) - xrpAmount := rippledata.Amount{ - Value: totalSendValue, - Currency: xrpl.XRPTokenCurrency, - Issuer: xrpl.XRPTokenIssuer, - } - err := bridgeClient.SendFromXRPLToCoreum( - ctx, xrplAccount.String(), xrpAmount, coreumAccount, - ) - require.NoError(t, err) - err = chains.Coreum.AwaitForBalance( - ctx, - t, - coreumAccount, - sdk.NewCoin( - registeredXrpDenomOnCoreum, - integrationtests.ConvertStringWithDecimalsToSDKInt( - t, - totalSendValue.String(), - xrpl.XRPCurrencyDecimals, - )), - ) - require.NoError(t, err) - - sdkIntAmount := sdkmath.NewInt(int64(math.Ceil(xrpToEachAccount.Float() * 1_000_000))) - msg := &banktypes.MsgMultiSend{ - Inputs: []banktypes.Input{{ - Address: coreumAccount.String(), - Coins: sdk.NewCoins(sdk.NewCoin(registeredXrpDenomOnCoreum, sdkIntAmount.MulRaw(int64(len(coreumAccounts))))), - }}, - Outputs: []banktypes.Output{}, - } - for _, acc := range coreumAccounts { - acc := acc - msg.Outputs = append(msg.Outputs, banktypes.Output{ - Address: acc.String(), - Coins: sdk.NewCoins(sdk.NewCoin(registeredXrpDenomOnCoreum, sdkIntAmount)), - }) - } - chains.Coreum.FundAccountWithOptions(ctx, t, coreumAccount, coreumintegration.BalancesOptions{ - Messages: []sdk.Msg{msg}, - }) - - _, err = client.BroadcastTx( - ctx, - chains.Coreum.ClientContext.WithFromAddress(coreumAccount), - chains.Coreum.TxFactory().WithSimulateAndExecute(true), - msg, - ) - require.NoError(t, err) -} - -func awaitXRPLBalance( - ctx context.Context, - t *testing.T, - xrpl integrationtests.XRPLChain, - account rippledata.Account, - issuer rippledata.Account, - currency rippledata.Currency, - expectedBalance rippledata.Value, -) { - t.Helper() - ctx, waitCancel := context.WithTimeout(ctx, 30*time.Second) - defer waitCancel() - - awaitState(ctx, t, func(t *testing.T) error { - balance := xrpl.GetAccountBalance(ctx, t, account, issuer, currency) - if !balance.Value.Equals(expectedBalance) { - return errors.Errorf("balance (%+v) is not euqal to expected (%+v)", balance.Value, expectedBalance) - } - return nil - }) -} - -func awaitPendingRefund( - ctx context.Context, - t *testing.T, - contractClient *coreum.ContractClient, - account sdk.AccAddress, -) []coreum.PendingRefund { - waitCtx, waitCancel := context.WithTimeout(ctx, 30*time.Second) - defer waitCancel() - t.Helper() - var refunds []coreum.PendingRefund - awaitState(waitCtx, t, func(t *testing.T) error { - var err error - refunds, err = contractClient.GetPendingRefunds(waitCtx, account) - if err != nil { - return err - } - if len(refunds) == 0 { - return errors.Errorf("no pending refunds for address %s", account.String()) - } - return nil - }) - return refunds -} - -func awaitState(ctx context.Context, t *testing.T, stateChecker func(t *testing.T) error) { - t.Helper() - err := retry.Do(ctx, 500*time.Millisecond, func() error { - if err := stateChecker(t); err != nil { - return retry.Retryable(err) - } - - return nil - }) - require.NoError(t, err) -} diff --git a/integration-tests/stress/stress_test.go b/integration-tests/stress/stress_test.go new file mode 100644 index 00000000..1dbac808 --- /dev/null +++ b/integration-tests/stress/stress_test.go @@ -0,0 +1,347 @@ +//go:build integrationtests +// +build integrationtests + +package stress_test + +import ( + "context" + "strconv" + "testing" + "time" + + sdkmath "cosmossdk.io/math" + sdk "github.com/cosmos/cosmos-sdk/types" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + "github.com/pkg/errors" + rippledata "github.com/rubblelabs/ripple/data" + "github.com/samber/lo" + "github.com/stretchr/testify/require" + + "github.com/CoreumFoundation/coreum-tools/pkg/parallel" + integrationtests "github.com/CoreumFoundation/coreumbridge-xrpl/integration-tests" + "github.com/CoreumFoundation/coreumbridge-xrpl/relayer/coreum" + "github.com/CoreumFoundation/coreumbridge-xrpl/relayer/xrpl" +) + +func TestStress(t *testing.T) { + t.Parallel() + + envCfg := DefaultEnvConfig() + env := NewEnv(t, envCfg) + + ctx, cancel := context.WithTimeout(context.Background(), envCfg.TestTimeout) + t.Cleanup(cancel) + + type testCase struct { + name string + testCase func(context.Context, *testing.T, *Env) + } + tests := []testCase{ + { + name: "send_XRP_from_XRPL_and_back", + testCase: sendXRPFromXRPLAndBack, + }, + { + name: "send_to_XRPL_with_failure_and_claim_refund", + testCase: sendWithFailureAndClaimRefund, + }, + { + name: "enable_and_disable_XRP_token", + testCase: enableAndDisableXRPToken, + }, + { + name: "halt_and_resume_bridge", + testCase: haltAndResumeBridge, + }, + { + name: "change_XRPL_base_fee_to_low_and_back", + testCase: changeXRPLBaseFeeToLowAndBack, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + t.Logf("Running test:%s", tt.name) + startTime := time.Now() + tt.testCase(ctx, t, env) + t.Logf("Test finished test:%s, time spent:%s", tt.name, time.Since(startTime)) + }) + } +} + +func sendXRPFromXRPLAndBack(ctx context.Context, t *testing.T, env *Env) { + coreumAccounts, xrplAccounts := env.GenCoreumAndXRPLAccounts( + ctx, + t, + env.Cfg.ParallelExecutionNumber, + sdkmath.NewIntWithDecimal(1, 5).MulRaw(int64(env.Cfg.RepeatTestCaseCount)), + env.Cfg.ParallelExecutionNumber, + 0.1, + ) + + valueToSendFromXRPLtoCoreum, err := rippledata.NewNativeValue(1) + require.NoError(t, err) + amountToSendFromXRPLtoCoreum := rippledata.Amount{ + Value: valueToSendFromXRPLtoCoreum, + Currency: xrpl.XRPTokenCurrency, + Issuer: xrpl.XRPTokenIssuer, + } + + registeredXRPToken, err := env.BridgeClient.GetXRPLTokenByIssuerAndCurrency( + ctx, xrpl.XRPTokenIssuer.String(), xrpl.ConvertCurrencyToString(xrpl.XRPTokenCurrency), + ) + require.NoError(t, err) + + coreumAmount := sdk.NewCoin( + registeredXRPToken.CoreumDenom, + integrationtests.ConvertStringWithDecimalsToSDKInt( + t, + valueToSendFromXRPLtoCoreum.String(), + xrpl.XRPCurrencyDecimals, + )) + + require.NoError(t, parallel.Run(ctx, func(ctx context.Context, spawn parallel.SpawnFn) error { + for i := 0; i < env.Cfg.ParallelExecutionNumber; i++ { + coreumAccount := coreumAccounts[i] + xrplAccount := xrplAccounts[i] + spawn(strconv.Itoa(i), parallel.Continue, func(ctx context.Context) error { + // get new instance of the bridge client to allow parallel execution for each account + bridgeClient := env.NewBridgeClient() + for j := 0; j < env.Cfg.RepeatTestCaseCount; j++ { + if err := func() error { + ctx, cancel := context.WithTimeout(ctx, env.Cfg.TestCaseTimeout) + defer cancel() + + if err := bridgeClient.SendFromXRPLToCoreum( + ctx, xrplAccount.String(), amountToSendFromXRPLtoCoreum, coreumAccount, + ); err != nil { + return err + } + + if err := env.AwaitCoreumBalance( + ctx, + coreumAccount, + coreumAmount, + ); err != nil { + return err + } + + xrpBalanceBefore, err := env.Chains.XRPL.RPCClient().GetXRPLBalance( + ctx, + xrplAccount, + xrpl.XRPTokenCurrency, + xrpl.XRPTokenIssuer, + ) + if err != nil { + return err + } + + // send back to XRPL + if err = env.AwaitContractCall(ctx, func() error { + return bridgeClient.SendFromCoreumToXRPL( + ctx, + coreumAccount, + xrplAccount, + coreumAmount, + nil, + ) + }); err != nil { + return err + } + + xrplValueAfter, err := xrpBalanceBefore.Value.Add(*valueToSendFromXRPLtoCoreum) + if err != nil { + return err + } + + return env.AwaitXRPLBalance(ctx, xrplAccount, rippledata.Amount{ + Value: xrplValueAfter, + Currency: xrpl.XRPTokenCurrency, + Issuer: xrpl.XRPTokenIssuer, + }) + }(); err != nil { + return err + } + } + return nil + }) + } + return nil + })) +} + +func sendWithFailureAndClaimRefund(ctx context.Context, t *testing.T, env *Env) { + coreumAccounts := env.GenCoreumAccounts( + ctx, + t, + env.Cfg.ParallelExecutionNumber, + sdkmath.NewIntWithDecimal(2, 5).MulRaw(int64(env.Cfg.RepeatTestCaseCount)), + ) + + amountToSendFromCoreumXRPL := sdkmath.NewInt(1) + + registeredXRPToken, err := env.BridgeClient.GetXRPLTokenByIssuerAndCurrency( + ctx, xrpl.XRPTokenIssuer.String(), xrpl.ConvertCurrencyToString(xrpl.XRPTokenCurrency), + ) + require.NoError(t, err) + env.FundCoreumAccountsWithXRP( + ctx, + t, + coreumAccounts, + amountToSendFromCoreumXRPL, + ) + + bankClient := banktypes.NewQueryClient(env.Chains.Coreum.ClientContext) + + require.NoError(t, parallel.Run(ctx, func(ctx context.Context, spawn parallel.SpawnFn) error { + for i := 0; i < env.Cfg.ParallelExecutionNumber; i++ { + coreumAccount := coreumAccounts[i] + spawn(strconv.Itoa(i), parallel.Continue, func(ctx context.Context) error { + // get new instance of the bridge client to allow parallel execution for each account + bridgeClient := env.NewBridgeClient() + for j := 0; j < env.Cfg.RepeatTestCaseCount; j++ { + if err := func() error { + ctx, cancel := context.WithTimeout(ctx, env.Cfg.TestCaseTimeout) + defer cancel() + + xrplAccount := xrpl.GenPrivKeyTxSigner().Account() + + if err = env.AwaitContractCall(ctx, func() error { + return bridgeClient.SendFromCoreumToXRPL( + ctx, + coreumAccount, + xrplAccount, + sdk.NewCoin( + registeredXRPToken.CoreumDenom, + amountToSendFromCoreumXRPL), + nil, + ) + }); err != nil { + return err + } + + refunds, err := env.AwaitPendingRefund(ctx, coreumAccount) + if err != nil { + return err + } + if len(refunds) != 1 { + return errors.Errorf("got unexpected number of refunds, refunds:%v", refunds) + } + balanceBefore, err := bankClient.Balance( + ctx, banktypes.NewQueryBalanceRequest(coreumAccount, registeredXRPToken.CoreumDenom), + ) + if err != nil { + return err + } + + if err = env.AwaitContractCall(ctx, func() error { + return bridgeClient.ClaimRefund(ctx, coreumAccount, refunds[0].ID) + }); err != nil { + return err + } + + balanceAfter, err := bankClient.Balance( + ctx, banktypes.NewQueryBalanceRequest(coreumAccount, registeredXRPToken.CoreumDenom), + ) + if err != nil { + return err + } + balanceChange := balanceAfter.GetBalance().Amount.Sub(balanceBefore.Balance.Amount) + if balanceChange.String() != amountToSendFromCoreumXRPL.String() { + return errors.Errorf( + "got unexpected balance change exected %s, got:%s", + balanceChange.String(), amountToSendFromCoreumXRPL.String(), + ) + } + + return nil + }(); err != nil { + return err + } + } + return nil + }) + } + return nil + })) +} + +func enableAndDisableXRPToken(ctx context.Context, t *testing.T, env *Env) { + require.NoError(t, env.RepeatOwnerActionWithDelay( + ctx, + func() error { + return env.BridgeClient.UpdateXRPLToken( + ctx, + env.ContractOwner, + xrpl.XRPTokenIssuer.String(), + xrpl.ConvertCurrencyToString(xrpl.XRPTokenCurrency), + lo.ToPtr(coreum.TokenStateDisabled), + nil, + nil, + nil, + ) + }, + func() error { + return env.BridgeClient.UpdateXRPLToken( + ctx, + env.ContractOwner, + xrpl.XRPTokenIssuer.String(), + xrpl.ConvertCurrencyToString(xrpl.XRPTokenCurrency), + lo.ToPtr(coreum.TokenStateEnabled), + nil, + nil, + nil, + ) + }, + ), + ) +} + +func haltAndResumeBridge(ctx context.Context, t *testing.T, env *Env) { + require.NoError(t, env.RepeatOwnerActionWithDelay( + ctx, + func() error { + return env.BridgeClient.HaltBridge( + ctx, + env.ContractOwner, + ) + }, + func() error { + return env.BridgeClient.ResumeBridge( + ctx, + env.ContractOwner, + ) + }, + ), + ) +} + +func changeXRPLBaseFeeToLowAndBack(ctx context.Context, t *testing.T, env *Env) { + contractCfg, err := env.BridgeClient.GetContractConfig(ctx) + require.NoError(t, err) + initialXRPLBaseFee := contractCfg.XRPLBaseFee + + require.NoError(t, env.RepeatOwnerActionWithDelay( + ctx, + func() error { + return env.BridgeClient.UpdateXRPLBaseFee( + ctx, + env.ContractOwner, + // low base XRPL fee, so the XRPL transactions won't pass + 1, + ) + }, + func() error { + return env.BridgeClient.UpdateXRPLBaseFee( + ctx, + env.ContractOwner, + // normal base XRPL fee, so the XRPL transactions pass + initialXRPLBaseFee, + ) + }, + ), + ) +} diff --git a/relayer/client/bridge.go b/relayer/client/bridge.go index 29f2d337..4ba4dd9d 100644 --- a/relayer/client/bridge.go +++ b/relayer/client/bridge.go @@ -582,6 +582,14 @@ func (b *BridgeClient) GetAllTokens(ctx context.Context) ([]coreum.CoreumToken, return coreumTokens, xrplTokens, nil } +// GetXRPLTokenByIssuerAndCurrency returns XRPL registered token by issuer and currency. +func (b *BridgeClient) GetXRPLTokenByIssuerAndCurrency( + ctx context.Context, + issuer, currency string, +) (coreum.XRPLToken, error) { + return b.contractClient.GetXRPLTokenByIssuerAndCurrency(ctx, issuer, currency) +} + // SendFromCoreumToXRPL sends tokens form Coreum to XRPL. func (b *BridgeClient) SendFromCoreumToXRPL( ctx context.Context, diff --git a/relayer/xrpl/rpc.go b/relayer/xrpl/rpc.go index 8381b4a6..e8fef5eb 100644 --- a/relayer/xrpl/rpc.go +++ b/relayer/xrpl/rpc.go @@ -240,6 +240,31 @@ func NewRPCClient(cfg RPCClientConfig, log logger.Logger, httpClient HTTPClient) } } +// GetXRPLBalance return account's XRPL balance. +func (c *RPCClient) GetXRPLBalance( + ctx context.Context, + acc rippledata.Account, + currency rippledata.Currency, + issuer rippledata.Account, +) (rippledata.Amount, error) { + balances, err := c.GetXRPLBalances(ctx, acc) + if err != nil { + return rippledata.Amount{}, err + } + for _, balance := range balances { + if balance.Currency.String() == currency.String() && + balance.Issuer.String() == issuer.String() { + return balance, nil + } + } + + return rippledata.Amount{ + Value: &rippledata.Value{}, + Currency: currency, + Issuer: issuer, + }, nil +} + // GetXRPLBalances returns all account's XRPL balances including XRP token. func (c *RPCClient) GetXRPLBalances(ctx context.Context, acc rippledata.Account) ([]rippledata.Amount, error) { balances := make([]rippledata.Amount, 0)