diff --git a/codec/depinject.go b/codec/depinject.go index c685a3763e15..d172a5813d9a 100644 --- a/codec/depinject.go +++ b/codec/depinject.go @@ -16,6 +16,13 @@ import ( "github.com/cosmos/cosmos-sdk/codec/types" ) +var DefaultProviders = depinject.Provide( + ProvideInterfaceRegistry, + ProvideLegacyAmino, + ProvideProtoCodec, + ProvideAddressCodec, +) + func ProvideInterfaceRegistry( addressCodec address.Codec, validatorAddressCodec address.ValidatorAddressCodec, diff --git a/server/v2/appmanager/appmanager.go b/server/v2/appmanager/appmanager.go index 9380efb0bf21..597d1c8f4110 100644 --- a/server/v2/appmanager/appmanager.go +++ b/server/v2/appmanager/appmanager.go @@ -207,17 +207,16 @@ func (a appManager[T]) SimulateWithState(ctx context.Context, state corestore.Re // Query queries the application at the provided version. // CONTRACT: Version must always be provided, if 0, get latest func (a appManager[T]) Query(ctx context.Context, version uint64, request transaction.Msg) (transaction.Msg, error) { + var ( + queryState corestore.ReaderMap + err error + ) // if version is provided attempt to do a height query. if version != 0 { - queryState, err := a.db.StateAt(version) - if err != nil { - return nil, err - } - return a.stf.Query(ctx, queryState, a.config.QueryGasLimit, request) + queryState, err = a.db.StateAt(version) + } else { // otherwise rely on latest available state. + _, queryState, err = a.db.StateLatest() } - - // otherwise rely on latest available state. - _, queryState, err := a.db.StateLatest() if err != nil { return nil, err } diff --git a/server/v2/stf/core_header_service.go b/server/v2/stf/core_header_service.go index 8b7f6c412be9..bef2ad8894ea 100644 --- a/server/v2/stf/core_header_service.go +++ b/server/v2/stf/core_header_service.go @@ -24,8 +24,6 @@ const headerInfoPrefix = 0x37 // setHeaderInfo sets the header info in the state to be used by queries in the future. func (s STF[T]) setHeaderInfo(state store.WriterMap, headerInfo header.Info) error { - // TODO storing header info is too low level here, stf should be stateless. - // We should have a keeper that does this. runtimeStore, err := state.GetWriter(Identity) if err != nil { return err diff --git a/simapp/v2/app_di.go b/simapp/v2/app_di.go index 4d6c3078cbed..ec506e764d09 100644 --- a/simapp/v2/app_di.go +++ b/simapp/v2/app_di.go @@ -48,11 +48,8 @@ func AppConfig() depinject.Config { return depinject.Configs( ModuleConfig, // Alternatively use appconfig.LoadYAML(AppConfigYAML) runtime.DefaultServiceBindings(), + codec.DefaultProviders, depinject.Provide( - codec.ProvideInterfaceRegistry, - codec.ProvideAddressCodec, - codec.ProvideProtoCodec, - codec.ProvideLegacyAmino, ProvideRootStoreConfig, // inject desired account types: multisigdepinject.ProvideAccount, diff --git a/tests/go.mod b/tests/go.mod index 13651b36201e..723fc33774f7 100644 --- a/tests/go.mod +++ b/tests/go.mod @@ -33,6 +33,9 @@ require ( require ( cosmossdk.io/core/testing v0.0.0-20240923163230-04da382a9f29 + cosmossdk.io/runtime/v2 v2.0.0-20240911143651-72620a577660 + cosmossdk.io/server/v2/stf v0.0.0-00010101000000-000000000000 + cosmossdk.io/store/v2 v2.0.0-00010101000000-000000000000 cosmossdk.io/x/accounts v0.0.0-20240913065641-0064ccbce64e cosmossdk.io/x/accounts/defaults/base v0.0.0-00010101000000-000000000000 cosmossdk.io/x/accounts/defaults/lockup v0.0.0-20240417181816-5e7aae0db1f5 @@ -52,6 +55,7 @@ require ( github.com/jhump/protoreflect v1.17.0 github.com/rs/zerolog v1.33.0 github.com/spf13/viper v1.19.0 + gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b ) require ( @@ -65,7 +69,9 @@ require ( cloud.google.com/go/storage v1.43.0 // indirect cosmossdk.io/client/v2 v2.0.0-20230630094428-02b760776860 // indirect cosmossdk.io/errors v1.0.1 // indirect + cosmossdk.io/errors/v2 v2.0.0-20240731132947-df72853b3ca5 // indirect cosmossdk.io/schema v0.3.1-0.20241010135032-192601639cac // indirect + cosmossdk.io/server/v2/appmanager v0.0.0-00010101000000-000000000000 // indirect cosmossdk.io/x/circuit v0.0.0-20230613133644-0a778132a60f // indirect cosmossdk.io/x/epochs v0.0.0-20240522060652-a1ae4c3e0337 // indirect filippo.io/edwards25519 v1.1.0 // indirect @@ -162,6 +168,7 @@ require ( github.com/manifoldco/promptui v0.9.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-sqlite3 v1.14.22 // indirect github.com/minio/highwayhash v1.0.3 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/go-testing-interface v1.14.1 // indirect @@ -196,7 +203,6 @@ require ( github.com/ulikunitz/xz v0.5.12 // indirect github.com/zondax/hid v0.9.2 // indirect github.com/zondax/ledger-go v0.14.3 // indirect - gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b // indirect gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 // indirect go.etcd.io/bbolt v1.4.0-alpha.0.0.20240404170359-43604f3112c5 // indirect go.opencensus.io v0.24.0 // indirect @@ -237,7 +243,11 @@ replace ( cosmossdk.io/api => ../api cosmossdk.io/client/v2 => ../client/v2 cosmossdk.io/collections => ../collections + cosmossdk.io/runtime/v2 => ../runtime/v2 + cosmossdk.io/server/v2/appmanager => ../server/v2/appmanager + cosmossdk.io/server/v2/stf => ../server/v2/stf cosmossdk.io/store => ../store + cosmossdk.io/store/v2 => ../store/v2 cosmossdk.io/x/accounts => ../x/accounts cosmossdk.io/x/accounts/defaults/base => ../x/accounts/defaults/base cosmossdk.io/x/accounts/defaults/lockup => ../x/accounts/defaults/lockup diff --git a/tests/go.sum b/tests/go.sum index 8aeebe04da0a..7a8afc7178c3 100644 --- a/tests/go.sum +++ b/tests/go.sum @@ -200,6 +200,8 @@ cosmossdk.io/depinject v1.0.0 h1:dQaTu6+O6askNXO06+jyeUAnF2/ssKwrrszP9t5q050= cosmossdk.io/depinject v1.0.0/go.mod h1:zxK/h3HgHoA/eJVtiSsoaRaRA2D5U4cJ5thIG4ssbB8= cosmossdk.io/errors v1.0.1 h1:bzu+Kcr0kS/1DuPBtUFdWjzLqyUuCiyHjyJB6srBV/0= cosmossdk.io/errors v1.0.1/go.mod h1:MeelVSZThMi4bEakzhhhE/CKqVv3nOJDA25bIqRDu/U= +cosmossdk.io/errors/v2 v2.0.0-20240731132947-df72853b3ca5 h1:IQNdY2kB+k+1OM2DvqFG1+UgeU1JzZrWtwuWzI3ZfwA= +cosmossdk.io/errors/v2 v2.0.0-20240731132947-df72853b3ca5/go.mod h1:0CuYKkFHxc1vw2JC+t21THBCALJVROrWVR/3PQ1urpc= cosmossdk.io/log v1.4.1 h1:wKdjfDRbDyZRuWa8M+9nuvpVYxrEOwbD/CA8hvhU8QM= cosmossdk.io/log v1.4.1/go.mod h1:k08v0Pyq+gCP6phvdI6RCGhLf/r425UT6Rk/m+o74rU= cosmossdk.io/math v1.3.0 h1:RC+jryuKeytIiictDslBP9i1fhkVm6ZDmZEoNP316zE= @@ -646,6 +648,8 @@ github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/minio/highwayhash v1.0.3 h1:kbnuUMoHYyVl7szWjSxJnxw11k2U709jqFPPmIUyD6Q= github.com/minio/highwayhash v1.0.3/go.mod h1:GGYsuwP/fPD6Y9hMiXuapVvlIUEhFhMTh0rxU3ik1LQ= diff --git a/tests/integration/v2/app.go b/tests/integration/v2/app.go new file mode 100644 index 000000000000..7684a539a209 --- /dev/null +++ b/tests/integration/v2/app.go @@ -0,0 +1,411 @@ +package integration + +import ( + "context" + "crypto/sha256" + "errors" + "fmt" + "math/rand" + "testing" + "time" + + cmtproto "github.com/cometbft/cometbft/api/cometbft/types/v1" + cmtjson "github.com/cometbft/cometbft/libs/json" + cmttypes "github.com/cometbft/cometbft/types" + "github.com/stretchr/testify/require" + + "cosmossdk.io/core/comet" + corecontext "cosmossdk.io/core/context" + "cosmossdk.io/core/server" + corestore "cosmossdk.io/core/store" + "cosmossdk.io/core/transaction" + "cosmossdk.io/depinject" + sdkmath "cosmossdk.io/math" + "cosmossdk.io/runtime/v2" + "cosmossdk.io/runtime/v2/services" + "cosmossdk.io/server/v2/stf" + "cosmossdk.io/server/v2/stf/branch" + "cosmossdk.io/store/v2" + "cosmossdk.io/store/v2/root" + bankkeeper "cosmossdk.io/x/bank/keeper" + banktypes "cosmossdk.io/x/bank/types" + consensustypes "cosmossdk.io/x/consensus/types" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + "github.com/cosmos/cosmos-sdk/std" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/types/tx/signing" + authsign "github.com/cosmos/cosmos-sdk/x/auth/signing" + "github.com/cosmos/cosmos-sdk/x/auth/tx" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" +) + +const DefaultGenTxGas = 10000000 +const ( + Genesis_COMMIT = iota + Genesis_NOCOMMIT + Genesis_SKIP +) + +type stateMachineTx = transaction.Tx + +// DefaultConsensusParams defines the default CometBFT consensus params used in +// SimApp testing. +var DefaultConsensusParams = &cmtproto.ConsensusParams{ + Version: &cmtproto.VersionParams{ + App: 1, + }, + Block: &cmtproto.BlockParams{ + MaxBytes: 200000, + MaxGas: 100_000_000, + }, + Evidence: &cmtproto.EvidenceParams{ + MaxAgeNumBlocks: 302400, + MaxAgeDuration: 504 * time.Hour, // 3 weeks is the max duration + MaxBytes: 10000, + }, + Validator: &cmtproto.ValidatorParams{ + PubKeyTypes: []string{ + cmttypes.ABCIPubKeyTypeEd25519, + cmttypes.ABCIPubKeyTypeSecp256k1, + }, + }, +} + +// StartupConfig defines the startup configuration of a new test app. +type StartupConfig struct { + // ValidatorSet defines a custom validator set to be validating the app. + ValidatorSet func() (*cmttypes.ValidatorSet, error) + // AppOption defines the additional operations that will be run in the app builder phase. + AppOption runtime.AppBuilderOption[stateMachineTx] + // GenesisBehavior defines the behavior of the app at genesis. + GenesisBehavior int + // GenesisAccounts defines the genesis accounts to be used in the app. + GenesisAccounts []GenesisAccount + // HomeDir defines the home directory of the app where config and data will be stored. + HomeDir string +} + +func DefaultStartUpConfig(t *testing.T) StartupConfig { + t.Helper() + + priv := secp256k1.GenPrivKey() + ba := authtypes.NewBaseAccount( + priv.PubKey().Address().Bytes(), + priv.PubKey(), + 0, + 0, + ) + ga := GenesisAccount{ + ba, + sdk.NewCoins( + sdk.NewCoin(sdk.DefaultBondDenom, sdkmath.NewInt(100000000000000)), + ), + } + homedir := t.TempDir() + t.Logf("generated integration test app config; HomeDir=%s", homedir) + return StartupConfig{ + ValidatorSet: CreateRandomValidatorSet, + GenesisBehavior: Genesis_COMMIT, + GenesisAccounts: []GenesisAccount{ga}, + HomeDir: homedir, + } +} + +// NewApp initializes a new runtime.App. A Nop logger is set in runtime.App. +// appConfig defines the application configuration (f.e. app_config.go). +// extraOutputs defines the extra outputs to be assigned by the dependency injector (depinject). +func NewApp( + appConfig depinject.Config, + startupConfig StartupConfig, + extraOutputs ...interface{}, +) (*App, error) { + // create the app with depinject + var ( + storeBuilder = root.NewBuilder() + app *runtime.App[stateMachineTx] + appBuilder *runtime.AppBuilder[stateMachineTx] + txConfig client.TxConfig + txConfigOptions tx.ConfigOptions + cometService comet.Service = &cometServiceImpl{} + kvFactory corestore.KVStoreServiceFactory = func(actor []byte) corestore.KVStoreService { + return services.NewGenesisKVService(actor, &storeService{actor, stf.NewKVStoreService(actor)}) + } + cdc codec.Codec + err error + ) + + if err := depinject.Inject( + depinject.Configs( + appConfig, + codec.DefaultProviders, + depinject.Supply( + &root.Config{ + Home: startupConfig.HomeDir, + AppDBBackend: "goleveldb", + Options: root.DefaultStoreOptions(), + }, + runtime.GlobalConfig{ + "server": server.ConfigMap{ + "minimum-gas-prices": "0stake", + }, + }, + services.NewGenesisHeaderService(stf.HeaderService{}), + cometService, + kvFactory, + &eventService{}, + storeBuilder, + ), + depinject.Invoke( + std.RegisterInterfaces, + ), + ), + append(extraOutputs, &appBuilder, &cdc, &txConfigOptions, &txConfig, &storeBuilder)...); err != nil { + return nil, fmt.Errorf("failed to inject dependencies: %w", err) + } + + app, err = appBuilder.Build() + if err != nil { + return nil, fmt.Errorf("failed to build app: %w", err) + } + if err := app.LoadLatest(); err != nil { + return nil, fmt.Errorf("failed to load app: %w", err) + } + + store := storeBuilder.Get() + if store == nil { + return nil, fmt.Errorf("failed to build store: %w", err) + } + err = store.SetInitialVersion(1) + if err != nil { + return nil, fmt.Errorf("failed to set initial version: %w", err) + } + + integrationApp := &App{App: app, Store: store, txConfig: txConfig, lastHeight: 1} + if startupConfig.GenesisBehavior == Genesis_SKIP { + return integrationApp, nil + } + + // create validator set + valSet, err := startupConfig.ValidatorSet() + if err != nil { + return nil, errors.New("failed to create validator set") + } + + var ( + balances []banktypes.Balance + genAccounts []authtypes.GenesisAccount + ) + for _, ga := range startupConfig.GenesisAccounts { + genAccounts = append(genAccounts, ga.GenesisAccount) + balances = append( + balances, + banktypes.Balance{ + Address: ga.GenesisAccount.GetAddress().String(), + Coins: ga.Coins, + }, + ) + } + + genesisJSON, err := genesisStateWithValSet( + cdc, + app.DefaultGenesis(), + valSet, + genAccounts, + balances...) + if err != nil { + return nil, fmt.Errorf("failed to create genesis state: %w", err) + } + + // init chain must be called to stop deliverState from being nil + genesisJSONBytes, err := cmtjson.MarshalIndent(genesisJSON, "", " ") + if err != nil { + return nil, fmt.Errorf( + "failed to marshal default genesis state: %w", + err, + ) + } + + ctx := context.WithValue( + context.Background(), + corecontext.CometParamsInitInfoKey, + &consensustypes.MsgUpdateParams{ + Authority: "consensus", + Block: DefaultConsensusParams.Block, + Evidence: DefaultConsensusParams.Evidence, + Validator: DefaultConsensusParams.Validator, + Abci: DefaultConsensusParams.Abci, + Synchrony: DefaultConsensusParams.Synchrony, + Feature: DefaultConsensusParams.Feature, + }, + ) + + emptyHash := sha256.Sum256(nil) + _, genesisState, err := app.InitGenesis( + ctx, + &server.BlockRequest[stateMachineTx]{ + Height: 1, + Time: time.Now(), + Hash: emptyHash[:], + ChainId: "test-chain", + AppHash: emptyHash[:], + IsGenesis: true, + }, + genesisJSONBytes, + &genesisTxCodec{txConfigOptions}, + ) + if err != nil { + return nil, fmt.Errorf("failed init genesis: %w", err) + } + + if startupConfig.GenesisBehavior == Genesis_NOCOMMIT { + integrationApp.lastHeight = 0 + return integrationApp, nil + } + + _, err = integrationApp.Commit(genesisState) + if err != nil { + return nil, fmt.Errorf("failed to commit initial version: %w", err) + } + + return integrationApp, nil +} + +// App is a wrapper around runtime.App that provides additional testing utilities. +type App struct { + *runtime.App[stateMachineTx] + lastHeight uint64 + Store store.RootStore + txConfig client.TxConfig +} + +// Deliver delivers a block with the given transactions and returns the resulting state. +func (a *App) Deliver( + t *testing.T, ctx context.Context, txs []stateMachineTx, +) (*server.BlockResponse, corestore.WriterMap) { + t.Helper() + req := &server.BlockRequest[stateMachineTx]{ + Height: a.lastHeight + 1, + Txs: txs, + Hash: make([]byte, 32), + AppHash: make([]byte, 32), + } + resp, state, err := a.DeliverBlock(ctx, req) + require.NoError(t, err) + a.lastHeight++ + return resp, state +} + +// StateLatestContext creates returns a new context from context.Background() with the latest state. +func (a *App) StateLatestContext(t *testing.T) context.Context { + t.Helper() + _, state, err := a.Store.StateLatest() + require.NoError(t, err) + writeableState := branch.DefaultNewWriterMap(state) + iCtx := &integrationContext{state: writeableState} + return context.WithValue(context.Background(), contextKey, iCtx) +} + +// Commit commits the given state and returns the new state hash. +func (a *App) Commit(state corestore.WriterMap) ([]byte, error) { + changes, err := state.GetStateChanges() + if err != nil { + return nil, fmt.Errorf("failed to get state changes: %w", err) + } + cs := &corestore.Changeset{Changes: changes} + return a.Store.Commit(cs) +} + +// SignCheckDeliver signs and checks the given messages and delivers them. +func (a *App) SignCheckDeliver( + t *testing.T, ctx context.Context, msgs []sdk.Msg, + chainID string, accNums, accSeqs []uint64, privateKeys []cryptotypes.PrivKey, + txErrString string, +) server.TxResult { + t.Helper() + + r := rand.New(rand.NewSource(time.Now().UnixNano())) + sigs := make([]signing.SignatureV2, len(privateKeys)) + + // create a random length memo + memo := simulation.RandStringOfLength(r, simulation.RandIntBetween(r, 0, 100)) + + signMode, err := authsign.APISignModeToInternal(a.txConfig.SignModeHandler().DefaultMode()) + require.NoError(t, err) + + // 1st round: set SignatureV2 with empty signatures, to set correct + // signer infos. + for i, p := range privateKeys { + sigs[i] = signing.SignatureV2{ + PubKey: p.PubKey(), + Data: &signing.SingleSignatureData{ + SignMode: signMode, + }, + Sequence: accSeqs[i], + } + } + + txBuilder := a.txConfig.NewTxBuilder() + err = txBuilder.SetMsgs(msgs...) + require.NoError(t, err) + err = txBuilder.SetSignatures(sigs...) + require.NoError(t, err) + txBuilder.SetMemo(memo) + txBuilder.SetFeeAmount(sdk.Coins{sdk.NewInt64Coin(sdk.DefaultBondDenom, 0)}) + txBuilder.SetGasLimit(DefaultGenTxGas) + + // 2nd round: once all signer infos are set, every signer can sign. + for i, p := range privateKeys { + signerData := authsign.SignerData{ + Address: sdk.AccAddress(p.PubKey().Address()).String(), + ChainID: chainID, + AccountNumber: accNums[i], + Sequence: accSeqs[i], + PubKey: p.PubKey(), + } + + signBytes, err := authsign.GetSignBytesAdapter( + ctx, a.txConfig.SignModeHandler(), signMode, signerData, + // todo why fetch twice? + txBuilder.GetTx()) + require.NoError(t, err) + sig, err := p.Sign(signBytes) + require.NoError(t, err) + sigs[i].Data.(*signing.SingleSignatureData).Signature = sig + } + err = txBuilder.SetSignatures(sigs...) + require.NoError(t, err) + + builtTx := txBuilder.GetTx() + blockResponse, blockState := a.Deliver(t, ctx, []stateMachineTx{builtTx}) + + require.Equal(t, 1, len(blockResponse.TxResults)) + txResult := blockResponse.TxResults[0] + if txErrString != "" { + require.ErrorContains(t, txResult.Error, txErrString) + } else { + require.NoError(t, txResult.Error) + } + + _, err = a.Commit(blockState) + require.NoError(t, err) + + return txResult +} + +// CheckBalance checks the balance of the given address. +func (a *App) CheckBalance( + t *testing.T, ctx context.Context, addr sdk.AccAddress, expected sdk.Coins, keeper bankkeeper.Keeper, +) { + t.Helper() + balances := keeper.GetAllBalances(ctx, addr) + require.Equal(t, expected, balances) +} + +func (a *App) Close() error { + return a.Store.Close() +} diff --git a/tests/integration/v2/bank/app_test.go b/tests/integration/v2/bank/app_test.go new file mode 100644 index 000000000000..22a6c66d5bb9 --- /dev/null +++ b/tests/integration/v2/bank/app_test.go @@ -0,0 +1,469 @@ +package bank + +import ( + "testing" + + "github.com/stretchr/testify/require" + secp256k1_internal "gitlab.com/yawning/secp256k1-voi" + "gitlab.com/yawning/secp256k1-voi/secec" + + "cosmossdk.io/depinject" + "cosmossdk.io/log" + sdkmath "cosmossdk.io/math" + _ "cosmossdk.io/x/accounts" + _ "cosmossdk.io/x/bank" + bankkeeper "cosmossdk.io/x/bank/keeper" + "cosmossdk.io/x/bank/testutil" + "cosmossdk.io/x/bank/types" + _ "cosmossdk.io/x/consensus" + _ "cosmossdk.io/x/distribution" + distrkeeper "cosmossdk.io/x/distribution/keeper" + _ "cosmossdk.io/x/gov" + govv1 "cosmossdk.io/x/gov/types/v1" + _ "cosmossdk.io/x/protocolpool" + _ "cosmossdk.io/x/staking" + stakingtypes "cosmossdk.io/x/staking/types" + + "github.com/cosmos/cosmos-sdk/client" + cdctestutil "github.com/cosmos/cosmos-sdk/codec/testutil" + "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" + cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types" + "github.com/cosmos/cosmos-sdk/tests/integration/v2" + "github.com/cosmos/cosmos-sdk/testutil/configurator" + sdk "github.com/cosmos/cosmos-sdk/types" + _ "github.com/cosmos/cosmos-sdk/x/auth" + _ "github.com/cosmos/cosmos-sdk/x/auth/tx/config" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" +) + +var ( + stablePrivateKey, _ = secec.NewPrivateKeyFromScalar(secp256k1_internal.NewScalarFromUint64(100)) + priv1 = &secp256k1.PrivKey{Key: stablePrivateKey.Bytes()} + addr1 = sdk.AccAddress(priv1.PubKey().Address()) + priv2 = secp256k1.GenPrivKey() + addr2 = sdk.AccAddress(priv2.PubKey().Address()) + addr3 = sdk.AccAddress(secp256k1.GenPrivKey().PubKey().Address()) + coins = sdk.Coins{sdk.NewInt64Coin("foocoin", 10)} + halfCoins = sdk.Coins{sdk.NewInt64Coin("foocoin", 5)} + moduleAccAddr = authtypes.NewModuleAddress(stakingtypes.BondedPoolName) +) + +type suite struct { + BankKeeper bankkeeper.Keeper + AccountKeeper types.AccountKeeper + DistributionKeeper distrkeeper.Keeper + App *integration.App + TxConfig client.TxConfig +} + +type expectedBalance struct { + addr sdk.AccAddress + coins sdk.Coins +} + +type appTestCase struct { + desc string + msgs []sdk.Msg + accNums []uint64 + accSeqs []uint64 + privKeys []cryptotypes.PrivKey + expectedBalances []expectedBalance + expInError []string +} + +func createTestSuite(t *testing.T, genesisAccounts []authtypes.GenesisAccount) suite { + t.Helper() + res := suite{} + + moduleConfigs := []configurator.ModuleOption{ + configurator.AccountsModule(), + configurator.AuthModule(), + configurator.StakingModule(), + configurator.TxModule(), + configurator.ValidateModule(), + configurator.ConsensusModule(), + configurator.BankModule(), + configurator.GovModule(), + configurator.DistributionModule(), + configurator.ProtocolPoolModule(), + } + var err error + startupCfg := integration.DefaultStartUpConfig(t) + var genAccounts []integration.GenesisAccount + for _, acc := range genesisAccounts { + genAccounts = append(genAccounts, integration.GenesisAccount{GenesisAccount: acc}) + } + startupCfg.GenesisAccounts = genAccounts + res.App, err = integration.NewApp( + depinject.Configs(configurator.NewAppV2Config(moduleConfigs...), depinject.Supply(log.NewNopLogger())), + startupCfg, + &res.BankKeeper, &res.AccountKeeper, &res.DistributionKeeper, &res.TxConfig) + require.NoError(t, err) + + return res +} + +func TestSendNotEnoughBalance(t *testing.T) { + acc := &authtypes.BaseAccount{ + Address: addr1.String(), + } + + genAccs := []authtypes.GenesisAccount{acc} + s := createTestSuite(t, genAccs) + ctx := s.App.StateLatestContext(t) + + err := testutil.FundAccount( + ctx, s.BankKeeper, addr1, + sdk.NewCoins(sdk.NewInt64Coin("foocoin", 67))) + require.NoError(t, err) + res1 := s.AccountKeeper.GetAccount(ctx, addr1) + require.NotNil(t, res1) + require.Equal(t, acc, res1.(*authtypes.BaseAccount)) + + origAccNum := res1.GetAccountNumber() + origSeq := res1.GetSequence() + addr1Str, err := s.AccountKeeper.AddressCodec().BytesToString(addr1) + require.NoError(t, err) + addr2Str, err := s.AccountKeeper.AddressCodec().BytesToString(addr2) + require.NoError(t, err) + sendMsg := types.NewMsgSend(addr1Str, addr2Str, sdk.Coins{sdk.NewInt64Coin("foocoin", 100)}) + + // TODO how to auto-advance height with app v2 interface? + s.App.SignCheckDeliver( + t, ctx, []sdk.Msg{sendMsg}, "", []uint64{origAccNum}, []uint64{origSeq}, + []cryptotypes.PrivKey{priv1}, + "spendable balance 67foocoin is smaller than 100foocoin", + ) + s.App.CheckBalance(t, ctx, addr1, sdk.Coins{sdk.NewInt64Coin("foocoin", 67)}, s.BankKeeper) + res2 := s.AccountKeeper.GetAccount(ctx, addr1) + require.NotNil(t, res2) + + require.Equal(t, origAccNum, res2.GetAccountNumber()) + require.Equal(t, origSeq+1, res2.GetSequence()) +} + +func TestMsgMultiSendWithAccounts(t *testing.T) { + addr1Str, err := cdctestutil.CodecOptions{}.GetAddressCodec().BytesToString(addr1) + require.NoError(t, err) + acc := &authtypes.BaseAccount{ + Address: addr1Str, + } + + addr2Str, err := cdctestutil.CodecOptions{}.GetAddressCodec().BytesToString(addr2) + require.NoError(t, err) + + moduleStrAddr, err := cdctestutil.CodecOptions{}.GetAddressCodec().BytesToString(moduleAccAddr) + require.NoError(t, err) + + genAccs := []authtypes.GenesisAccount{acc} + s := createTestSuite(t, genAccs) + ctx := s.App.StateLatestContext(t) + + require.NoError(t, testutil.FundAccount(ctx, s.BankKeeper, addr1, sdk.NewCoins(sdk.NewInt64Coin("foocoin", 67)))) + + _, state := s.App.Deliver(t, ctx, nil) + _, err = s.App.Commit(state) + require.NoError(t, err) + + res1 := s.AccountKeeper.GetAccount(ctx, addr1) + require.NotNil(t, res1) + require.Equal(t, acc, res1.(*authtypes.BaseAccount)) + + testCases := []appTestCase{ + { + desc: "make a valid tx", + msgs: []sdk.Msg{&types.MsgMultiSend{ + Inputs: []types.Input{types.NewInput(addr1Str, coins)}, + Outputs: []types.Output{types.NewOutput(addr2Str, coins)}, + }}, + accNums: []uint64{0}, + accSeqs: []uint64{0}, + privKeys: []cryptotypes.PrivKey{priv1}, + expectedBalances: []expectedBalance{ + {addr1, sdk.Coins{sdk.NewInt64Coin("foocoin", 57)}}, + {addr2, sdk.Coins{sdk.NewInt64Coin("foocoin", 10)}}, + }, + }, + { + desc: "wrong accNum should pass Simulate, but not Deliver", + msgs: []sdk.Msg{&types.MsgMultiSend{ + Inputs: []types.Input{types.NewInput(addr1Str, coins)}, + Outputs: []types.Output{types.NewOutput(addr2Str, coins)}, + }}, + accNums: []uint64{1}, // wrong account number + accSeqs: []uint64{1}, + expInError: []string{"signature verification failed; please verify account number"}, + privKeys: []cryptotypes.PrivKey{priv1}, + }, + { + desc: "wrong accSeq should not pass Simulate", + msgs: []sdk.Msg{&types.MsgMultiSend{ + Inputs: []types.Input{types.NewInput(addr1Str, coins)}, + Outputs: []types.Output{ + types.NewOutput(moduleStrAddr, coins), + }, + }}, + accNums: []uint64{0}, + accSeqs: []uint64{0}, // wrong account sequence + expInError: []string{"account sequence mismatch"}, + privKeys: []cryptotypes.PrivKey{priv1}, + }, + { + desc: "multiple inputs not allowed", + msgs: []sdk.Msg{&types.MsgMultiSend{ + Inputs: []types.Input{types.NewInput(addr1Str, coins), types.NewInput(addr2Str, coins)}, + Outputs: []types.Output{}, + }}, + accNums: []uint64{0}, + accSeqs: []uint64{0}, + expInError: []string{"invalid number of signatures"}, + privKeys: []cryptotypes.PrivKey{priv1}, + }, + } + + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + var errString string + if len(tc.expInError) > 0 { + errString = tc.expInError[0] + } + s.App.SignCheckDeliver(t, ctx, tc.msgs, "", tc.accNums, tc.accSeqs, tc.privKeys, errString) + + for _, eb := range tc.expectedBalances { + s.App.CheckBalance(t, ctx, eb.addr, eb.coins, s.BankKeeper) + } + }) + } +} + +func TestMsgMultiSendMultipleOut(t *testing.T) { + ac := cdctestutil.CodecOptions{}.GetAddressCodec() + addr1Str, err := ac.BytesToString(addr1) + require.NoError(t, err) + acc1 := &authtypes.BaseAccount{ + Address: addr1Str, + } + addr2Str, err := ac.BytesToString(addr2) + require.NoError(t, err) + acc2 := &authtypes.BaseAccount{ + Address: addr2Str, + } + addr3Str, err := ac.BytesToString(addr3) + require.NoError(t, err) + + genAccs := []authtypes.GenesisAccount{acc1, acc2} + s := createTestSuite(t, genAccs) + ctx := s.App.StateLatestContext(t) + + require.NoError(t, testutil.FundAccount(ctx, s.BankKeeper, addr1, sdk.NewCoins(sdk.NewInt64Coin("foocoin", 42)))) + require.NoError(t, testutil.FundAccount(ctx, s.BankKeeper, addr2, sdk.NewCoins(sdk.NewInt64Coin("foocoin", 42)))) + _, state := s.App.Deliver(t, ctx, nil) + _, err = s.App.Commit(state) + require.NoError(t, err) + + testCases := []appTestCase{ + { + msgs: []sdk.Msg{&types.MsgMultiSend{ + Inputs: []types.Input{types.NewInput(addr1Str, coins)}, + Outputs: []types.Output{ + types.NewOutput(addr2Str, halfCoins), + types.NewOutput(addr3Str, halfCoins), + }, + }}, + accNums: []uint64{0}, + accSeqs: []uint64{0}, + privKeys: []cryptotypes.PrivKey{priv1}, + expectedBalances: []expectedBalance{ + {addr1, sdk.Coins{sdk.NewInt64Coin("foocoin", 32)}}, + {addr2, sdk.Coins{sdk.NewInt64Coin("foocoin", 47)}}, + {addr3, sdk.Coins{sdk.NewInt64Coin("foocoin", 5)}}, + }, + }, + } + + for _, tc := range testCases { + s.App.SignCheckDeliver(t, ctx, tc.msgs, "", tc.accNums, tc.accSeqs, tc.privKeys, "") + + for _, eb := range tc.expectedBalances { + s.App.CheckBalance(t, ctx, eb.addr, eb.coins, s.BankKeeper) + } + } +} + +func TestMsgMultiSendDependent(t *testing.T) { + ac := cdctestutil.CodecOptions{}.GetAddressCodec() + addr1Str, err := ac.BytesToString(addr1) + require.NoError(t, err) + addr2Str, err := ac.BytesToString(addr2) + require.NoError(t, err) + + acc1 := authtypes.NewBaseAccountWithAddress(addr1) + acc2 := authtypes.NewBaseAccountWithAddress(addr2) + err = acc2.SetAccountNumber(1) + require.NoError(t, err) + + genAccs := []authtypes.GenesisAccount{acc1, acc2} + s := createTestSuite(t, genAccs) + ctx := s.App.StateLatestContext(t) + + require.NoError(t, testutil.FundAccount(ctx, s.BankKeeper, addr1, sdk.NewCoins(sdk.NewInt64Coin("foocoin", 42)))) + _, state := s.App.Deliver(t, ctx, nil) + _, err = s.App.Commit(state) + require.NoError(t, err) + + testCases := []appTestCase{ + { + msgs: []sdk.Msg{&types.MsgMultiSend{ + Inputs: []types.Input{types.NewInput(addr1Str, coins)}, + Outputs: []types.Output{types.NewOutput(addr2Str, coins)}, + }}, + accNums: []uint64{0}, + accSeqs: []uint64{0}, + privKeys: []cryptotypes.PrivKey{priv1}, + expectedBalances: []expectedBalance{ + {addr1, sdk.Coins{sdk.NewInt64Coin("foocoin", 32)}}, + {addr2, sdk.Coins{sdk.NewInt64Coin("foocoin", 10)}}, + }, + }, + { + msgs: []sdk.Msg{&types.MsgMultiSend{ + Inputs: []types.Input{types.NewInput(addr2Str, coins)}, + Outputs: []types.Output{ + types.NewOutput(addr1Str, coins), + }, + }}, + accNums: []uint64{1}, + accSeqs: []uint64{0}, + privKeys: []cryptotypes.PrivKey{priv2}, + expectedBalances: []expectedBalance{ + {addr1, sdk.Coins{sdk.NewInt64Coin("foocoin", 42)}}, + }, + }, + } + + for _, tc := range testCases { + s.App.SignCheckDeliver(t, ctx, tc.msgs, "", tc.accNums, tc.accSeqs, tc.privKeys, "") + + for _, eb := range tc.expectedBalances { + s.App.CheckBalance(t, ctx, eb.addr, eb.coins, s.BankKeeper) + } + } +} + +func TestMsgSetSendEnabled(t *testing.T) { + acc1 := authtypes.NewBaseAccountWithAddress(addr1) + + genAccs := []authtypes.GenesisAccount{acc1} + s := createTestSuite(t, genAccs) + + ctx := s.App.StateLatestContext(t) + require.NoError(t, testutil.FundAccount(ctx, s.BankKeeper, addr1, sdk.NewCoins(sdk.NewInt64Coin("foocoin", 101)))) + require.NoError(t, testutil.FundAccount(ctx, s.BankKeeper, addr1, sdk.NewCoins(sdk.NewInt64Coin("stake", 100000)))) + addr1Str := addr1.String() + govAddr := s.BankKeeper.GetAuthority() + goodGovProp, err := govv1.NewMsgSubmitProposal( + []sdk.Msg{ + types.NewMsgSetSendEnabled(govAddr, nil, nil), + }, + sdk.Coins{{Denom: "stake", Amount: sdkmath.NewInt(100000)}}, + addr1Str, + "set default send enabled to true", + "Change send enabled", + "Modify send enabled and set to true", + govv1.ProposalType_PROPOSAL_TYPE_STANDARD, + ) + require.NoError(t, err, "making goodGovProp") + + testCases := []appTestCase{ + { + desc: "wrong authority", + msgs: []sdk.Msg{ + types.NewMsgSetSendEnabled(addr1Str, nil, nil), + }, + accSeqs: []uint64{0}, + expInError: []string{ + "invalid authority", + "cosmos10d07y265gmmuvt4z0w9aw880jnsr700j6zn9kn", + addr1Str, + "expected authority account as only signer for proposal message", + }, + }, + { + desc: "right authority wrong signer", + msgs: []sdk.Msg{ + types.NewMsgSetSendEnabled(govAddr, nil, nil), + }, + accSeqs: []uint64{1}, // wrong signer, so this sequence doesn't actually get used. + expInError: []string{ + "cannot be claimed by public key with address", + govAddr, + }, + }, + { + desc: "submitted good as gov prop", + msgs: []sdk.Msg{ + goodGovProp, + }, + accSeqs: []uint64{1}, + expInError: nil, + }, + } + + for _, tc := range testCases { + t.Run(tc.desc, func(tt *testing.T) { + var errString string + if len(tc.expInError) > 0 { + errString = tc.expInError[0] + } + txResult := s.App.SignCheckDeliver( + tt, ctx, tc.msgs, "", []uint64{0}, tc.accSeqs, []cryptotypes.PrivKey{priv1}, errString) + if len(tc.expInError) > 0 { + require.Error(tt, txResult.Error) + for _, exp := range tc.expInError { + require.ErrorContains(tt, txResult.Error, exp) + } + } else { + require.NoError(tt, txResult.Error) + } + }) + } +} + +// TestSendToNonExistingAccount tests sending coins to an account that does not exist, and this account +// must not be created. +func TestSendToNonExistingAccount(t *testing.T) { + acc1 := authtypes.NewBaseAccountWithAddress(addr1) + genAccs := []authtypes.GenesisAccount{acc1} + s := createTestSuite(t, genAccs) + ctx := s.App.StateLatestContext(t) + + require.NoError(t, testutil.FundAccount(ctx, s.BankKeeper, addr1, sdk.NewCoins(sdk.NewInt64Coin("foocoin", 42)))) + _, state := s.App.Deliver(t, ctx, nil) + _, err := s.App.Commit(state) + require.NoError(t, err) + + addr2Str, err := s.AccountKeeper.AddressCodec().BytesToString(addr2) + require.NoError(t, err) + sendMsg := types.NewMsgSend(addr1.String(), addr2Str, coins) + res := s.App.SignCheckDeliver(t, ctx, []sdk.Msg{sendMsg}, "", []uint64{0}, []uint64{0}, []cryptotypes.PrivKey{priv1}, "") + require.NoError(t, res.Error) + + // Check that the account was not created + acc2 := s.AccountKeeper.GetAccount(ctx, addr2) + require.Nil(t, acc2) + + // But it does have a balance + s.App.CheckBalance(t, ctx, addr2, coins, s.BankKeeper) + + // Now we send coins back and the account should be created + sendMsg = types.NewMsgSend(addr2Str, addr1.String(), coins) + res = s.App.SignCheckDeliver(t, ctx, []sdk.Msg{sendMsg}, "", []uint64{0}, []uint64{0}, []cryptotypes.PrivKey{priv2}, "") + require.NoError(t, res.Error) + + // Balance has been reduced + s.App.CheckBalance(t, ctx, addr2, sdk.NewCoins(), s.BankKeeper) + + // Check that the account was created + acc2 = s.AccountKeeper.GetAccount(ctx, addr2) + require.NotNil(t, acc2, "account should have been created %s", addr2.String()) +} diff --git a/tests/integration/v2/bank/determinisitic_test.go b/tests/integration/v2/bank/determinisitic_test.go new file mode 100644 index 000000000000..28995a0e3f6f --- /dev/null +++ b/tests/integration/v2/bank/determinisitic_test.go @@ -0,0 +1,570 @@ +package bank + +import ( + "context" + "fmt" + "testing" + + "github.com/cosmos/gogoproto/proto" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" + "pgregory.net/rapid" + + "cosmossdk.io/core/gas" + "cosmossdk.io/depinject" + "cosmossdk.io/log" + "cosmossdk.io/math" + bankkeeper "cosmossdk.io/x/bank/keeper" + banktestutil "cosmossdk.io/x/bank/testutil" + banktypes "cosmossdk.io/x/bank/types" + minttypes "cosmossdk.io/x/mint/types" + + codectestutil "github.com/cosmos/cosmos-sdk/codec/testutil" + "github.com/cosmos/cosmos-sdk/tests/integration/v2" + "github.com/cosmos/cosmos-sdk/testutil/configurator" + "github.com/cosmos/cosmos-sdk/testutil/testdata" + sdk "github.com/cosmos/cosmos-sdk/types" + authtestutil "github.com/cosmos/cosmos-sdk/x/auth/testutil" +) + +var ( + denomRegex = `[a-zA-Z][a-zA-Z0-9/:._-]{2,127}` + coin1 = sdk.NewCoin("denom", math.NewInt(10)) + metadataAtom = banktypes.Metadata{ + Description: "The native staking token of the Cosmos Hub.", + DenomUnits: []*banktypes.DenomUnit{ + { + Denom: "uatom", + Exponent: 0, + Aliases: []string{"microatom"}, + }, + { + Denom: "atom", + Exponent: 6, + Aliases: []string{"ATOM"}, + }, + }, + Base: "uatom", + Display: "atom", + } +) + +type deterministicFixture struct { + *testing.T + ctx context.Context + app *integration.App + bankKeeper bankkeeper.Keeper +} + +func queryFnFactory[RequestT, ResponseT proto.Message]( + f *deterministicFixture, +) func(RequestT) (ResponseT, error) { + return func(req RequestT) (ResponseT, error) { + var emptyResponse ResponseT + res, err := f.app.Query(f.ctx, 0, req) + if err != nil { + return emptyResponse, err + } + castedRes, ok := res.(ResponseT) + if !ok { + return emptyResponse, fmt.Errorf("unexpected response type: %T", res) + } + return castedRes, nil + } +} + +func fundAccount(f *deterministicFixture, addr sdk.AccAddress, coin ...sdk.Coin) { + err := banktestutil.FundAccount(f.ctx, f.bankKeeper, addr, sdk.NewCoins(coin...)) + require.NoError(f.T, err) +} + +func getCoin(rt *rapid.T) sdk.Coin { + return sdk.NewCoin( + rapid.StringMatching(denomRegex).Draw(rt, "denom"), + math.NewInt(rapid.Int64Min(1).Draw(rt, "amount")), + ) +} + +func initDeterministicFixture(t *testing.T) *deterministicFixture { + t.Helper() + + ctrl := gomock.NewController(t) + acctsModKeeper := authtestutil.NewMockAccountsModKeeper(ctrl) + accNum := uint64(0) + acctsModKeeper.EXPECT().NextAccountNumber(gomock.Any()).AnyTimes().DoAndReturn(func(ctx context.Context) ( + uint64, error, + ) { + currentNum := accNum + accNum++ + return currentNum, nil + }) + + startupConfig := integration.DefaultStartUpConfig(t) + startupConfig.GenesisBehavior = integration.Genesis_SKIP + diConfig := configurator.NewAppV2Config( + configurator.TxModule(), + configurator.AuthModule(), + configurator.BankModule(), + ) + + var bankKeeper bankkeeper.Keeper + diConfig = depinject.Configs(diConfig, depinject.Supply(acctsModKeeper, log.NewNopLogger())) + app, err := integration.NewApp(diConfig, startupConfig, &bankKeeper) + require.NoError(t, err) + require.NotNil(t, app) + return &deterministicFixture{app: app, bankKeeper: bankKeeper, T: t} +} + +func assertNonZeroGas(t *testing.T, gasUsed gas.Gas) { + t.Helper() + require.NotZero(t, gasUsed) +} + +func TestQueryBalance(t *testing.T) { + t.Parallel() + f := initDeterministicFixture(t) + f.ctx = f.app.StateLatestContext(t) + gasMeterFactory := integration.GasMeterFactory(f.ctx) + queryFn := queryFnFactory[*banktypes.QueryBalanceRequest, *banktypes.QueryBalanceResponse](f) + assertBalance := func(coin sdk.Coin) func(t *testing.T, res *banktypes.QueryBalanceResponse) { + return func(t *testing.T, res *banktypes.QueryBalanceResponse) { + t.Helper() + require.Equal(t, coin.Denom, res.Balance.Denom) + require.Truef(t, coin.Amount.Equal(res.Balance.Amount), + "expected %s, got %s", coin.Amount, res.Balance.Amount) + } + } + + rapid.Check(t, func(rt *rapid.T) { + addr := testdata.AddressGenerator(rt).Draw(rt, "address") + coin := getCoin(rt) + fundAccount(f, addr, coin) + + addrStr, err := codectestutil.CodecOptions{}.GetAddressCodec().BytesToString(addr) + require.NoError(t, err) + + req := banktypes.NewQueryBalanceRequest(addrStr, coin.GetDenom()) + + testdata.DeterministicIterationsV2(t, req, gasMeterFactory, queryFn, assertNonZeroGas, assertBalance(coin)) + }) + + fundAccount(f, addr1, coin1) + addr1Str, err := codectestutil.CodecOptions{}.GetAddressCodec().BytesToString(addr1) + require.NoError(t, err) + req := banktypes.NewQueryBalanceRequest(addr1Str, coin1.GetDenom()) + testdata.DeterministicIterationsV2(t, req, gasMeterFactory, queryFn, assertNonZeroGas, assertBalance(coin1)) +} + +func TestQueryAllBalances(t *testing.T) { + t.Parallel() + f := initDeterministicFixture(t) + f.ctx = f.app.StateLatestContext(t) + gasMeterFactory := integration.GasMeterFactory(f.ctx) + addressCodec := codectestutil.CodecOptions{}.GetAddressCodec() + queryFn := queryFnFactory[*banktypes.QueryAllBalancesRequest, *banktypes.QueryAllBalancesResponse](f) + + rapid.Check(t, func(rt *rapid.T) { + addr := testdata.AddressGenerator(rt).Draw(rt, "address") + numCoins := rapid.IntRange(1, 10).Draw(rt, "num-count") + coins := make(sdk.Coins, 0, numCoins) + + addrStr, err := addressCodec.BytesToString(addr) + require.NoError(t, err) + + for i := 0; i < numCoins; i++ { + coin := getCoin(rt) + if exists, _ := coins.Find(coin.Denom); exists { + t.Skip("duplicate denom") + } + // NewCoins sorts the denoms + coins = sdk.NewCoins(append(coins, coin)...) + } + + fundAccount(f, addr, coins...) + + req := banktypes.NewQueryAllBalancesRequest( + addrStr, testdata.PaginationGenerator(rt, uint64(numCoins)).Draw(rt, "pagination"), false) + testdata.DeterministicIterationsV2(t, req, gasMeterFactory, queryFn, assertNonZeroGas, nil) + }) + + coins := sdk.NewCoins( + sdk.NewCoin("stake", math.NewInt(10)), + sdk.NewCoin("denom", math.NewInt(100)), + ) + + fundAccount(f, addr1, coins...) + addr1Str, err := addressCodec.BytesToString(addr1) + require.NoError(t, err) + + req := banktypes.NewQueryAllBalancesRequest(addr1Str, nil, false) + testdata.DeterministicIterationsV2(t, req, gasMeterFactory, queryFn, assertNonZeroGas, nil) +} + +func TestQuerySpendableBalances(t *testing.T) { + t.Parallel() + f := initDeterministicFixture(t) + f.ctx = f.app.StateLatestContext(t) + gasMeterFactory := integration.GasMeterFactory(f.ctx) + queryFn := queryFnFactory[*banktypes.QuerySpendableBalancesRequest, *banktypes.QuerySpendableBalancesResponse](f) + + rapid.Check(t, func(rt *rapid.T) { + addr := testdata.AddressGenerator(rt).Draw(rt, "address") + addrStr, err := codectestutil.CodecOptions{}.GetAddressCodec().BytesToString(addr) + require.NoError(t, err) + + // Denoms must be unique, otherwise sdk.NewCoins will panic. + denoms := rapid.SliceOfNDistinct(rapid.StringMatching(denomRegex), 1, 10, rapid.ID[string]).Draw(rt, "denoms") + coins := make(sdk.Coins, 0, len(denoms)) + for _, denom := range denoms { + coin := sdk.NewCoin( + denom, + math.NewInt(rapid.Int64Min(1).Draw(rt, "amount")), + ) + + // NewCoins sorts the denoms + coins = sdk.NewCoins(append(coins, coin)...) + } + + err = banktestutil.FundAccount(f.ctx, f.bankKeeper, addr, coins) + require.NoError(t, err) + + req := banktypes.NewQuerySpendableBalancesRequest(addrStr, testdata.PaginationGenerator(rt, uint64(len(denoms))).Draw(rt, "pagination")) + testdata.DeterministicIterationsV2(t, req, gasMeterFactory, queryFn, assertNonZeroGas, nil) + }) + + coins := sdk.NewCoins( + sdk.NewCoin("stake", math.NewInt(10)), + sdk.NewCoin("denom", math.NewInt(100)), + ) + + err := banktestutil.FundAccount(f.ctx, f.bankKeeper, addr1, coins) + require.NoError(t, err) + + addr1Str, err := codectestutil.CodecOptions{}.GetAddressCodec().BytesToString(addr1) + require.NoError(t, err) + + req := banktypes.NewQuerySpendableBalancesRequest(addr1Str, nil) + testdata.DeterministicIterationsV2(t, req, gasMeterFactory, queryFn, assertNonZeroGas, nil) +} + +func TestQueryTotalSupply(t *testing.T) { + t.Parallel() + f := initDeterministicFixture(t) + f.ctx = f.app.StateLatestContext(t) + gasMeterFactory := integration.GasMeterFactory(f.ctx) + queryFn := queryFnFactory[*banktypes.QueryTotalSupplyRequest, *banktypes.QueryTotalSupplyResponse](f) + + res, err := queryFn(&banktypes.QueryTotalSupplyRequest{}) + require.NoError(t, err) + initialSupply := res.GetSupply() + + rapid.Check(t, func(rt *rapid.T) { + numCoins := rapid.IntRange(1, 3).Draw(rt, "num-count") + coins := make(sdk.Coins, 0, numCoins) + + for i := 0; i < numCoins; i++ { + coin := sdk.NewCoin( + rapid.StringMatching(denomRegex).Draw(rt, "denom"), + math.NewInt(rapid.Int64Min(1).Draw(rt, "amount")), + ) + + coins = coins.Add(coin) + } + + require.NoError(t, f.bankKeeper.MintCoins(f.ctx, minttypes.ModuleName, coins)) + + initialSupply = initialSupply.Add(coins...) + + req := &banktypes.QueryTotalSupplyRequest{ + Pagination: testdata.PaginationGenerator(rt, uint64(len(initialSupply))).Draw(rt, "pagination"), + } + + testdata.DeterministicIterationsV2(t, req, gasMeterFactory, queryFn, assertNonZeroGas, nil) + }) + + f = initDeterministicFixture(t) // reset + f.ctx = f.app.StateLatestContext(t) + gasMeterFactory = integration.GasMeterFactory(f.ctx) + queryFn = queryFnFactory[*banktypes.QueryTotalSupplyRequest, *banktypes.QueryTotalSupplyResponse](f) + + coins := sdk.NewCoins( + sdk.NewCoin("foo", math.NewInt(10)), + sdk.NewCoin("bar", math.NewInt(100)), + ) + + require.NoError(t, f.bankKeeper.MintCoins(f.ctx, minttypes.ModuleName, coins)) + + req := &banktypes.QueryTotalSupplyRequest{} + testdata.DeterministicIterationsV2(t, req, gasMeterFactory, queryFn, assertNonZeroGas, nil) +} + +func TestQueryTotalSupplyOf(t *testing.T) { + t.Parallel() + f := initDeterministicFixture(t) + f.ctx = f.app.StateLatestContext(t) + gasMeterFactory := integration.GasMeterFactory(f.ctx) + queryFn := queryFnFactory[*banktypes.QuerySupplyOfRequest, *banktypes.QuerySupplyOfResponse](f) + + rapid.Check(t, func(rt *rapid.T) { + coin := sdk.NewCoin( + rapid.StringMatching(denomRegex).Draw(rt, "denom"), + math.NewInt(rapid.Int64Min(1).Draw(rt, "amount")), + ) + + require.NoError(t, f.bankKeeper.MintCoins(f.ctx, minttypes.ModuleName, sdk.NewCoins(coin))) + + req := &banktypes.QuerySupplyOfRequest{Denom: coin.GetDenom()} + testdata.DeterministicIterationsV2(t, req, gasMeterFactory, queryFn, assertNonZeroGas, nil) + }) + + coin := sdk.NewCoin("bar", math.NewInt(100)) + + require.NoError(t, f.bankKeeper.MintCoins(f.ctx, minttypes.ModuleName, sdk.NewCoins(coin))) + req := &banktypes.QuerySupplyOfRequest{Denom: coin.GetDenom()} + testdata.DeterministicIterationsV2(t, req, gasMeterFactory, queryFn, assertNonZeroGas, nil) +} + +func TestQueryParams(t *testing.T) { + t.Parallel() + f := initDeterministicFixture(t) + f.ctx = f.app.StateLatestContext(t) + gasMeterFactory := integration.GasMeterFactory(f.ctx) + queryFn := queryFnFactory[*banktypes.QueryParamsRequest, *banktypes.QueryParamsResponse](f) + + rapid.Check(t, func(rt *rapid.T) { + enabledStatus := banktypes.SendEnabled{ + Denom: rapid.StringMatching(denomRegex).Draw(rt, "denom"), + Enabled: rapid.Bool().Draw(rt, "status"), + } + + params := banktypes.Params{ + SendEnabled: []*banktypes.SendEnabled{&enabledStatus}, + DefaultSendEnabled: rapid.Bool().Draw(rt, "send"), + } + + err := f.bankKeeper.SetParams(f.ctx, params) + require.NoError(t, err) + + req := &banktypes.QueryParamsRequest{} + testdata.DeterministicIterationsV2(t, req, gasMeterFactory, queryFn, assertNonZeroGas, nil) + }) + + enabledStatus := banktypes.SendEnabled{ + Denom: "denom", + Enabled: true, + } + + params := banktypes.Params{ + SendEnabled: []*banktypes.SendEnabled{&enabledStatus}, + DefaultSendEnabled: false, + } + + err := f.bankKeeper.SetParams(f.ctx, params) + require.NoError(t, err) + req := &banktypes.QueryParamsRequest{} + testdata.DeterministicIterationsV2(t, req, gasMeterFactory, queryFn, assertNonZeroGas, nil) +} + +func createAndReturnMetadatas(t *rapid.T, count int) []banktypes.Metadata { + denomsMetadata := make([]banktypes.Metadata, 0, count) + for i := 0; i < count; i++ { + + denom := rapid.StringMatching(denomRegex).Draw(t, "denom") + + aliases := rapid.SliceOf(rapid.String()).Draw(t, "aliases") + // In the GRPC server code, empty arrays are returned as nil + if len(aliases) == 0 { + aliases = nil + } + + metadata := banktypes.Metadata{ + Description: rapid.StringN(1, 100, 100).Draw(t, "desc"), + DenomUnits: []*banktypes.DenomUnit{ + { + Denom: denom, + Exponent: rapid.Uint32().Draw(t, "exponent"), + Aliases: aliases, + }, + }, + Base: denom, + Display: denom, + Name: rapid.String().Draw(t, "name"), + Symbol: rapid.String().Draw(t, "symbol"), + URI: rapid.String().Draw(t, "uri"), + URIHash: rapid.String().Draw(t, "uri-hash"), + } + + denomsMetadata = append(denomsMetadata, metadata) + } + + return denomsMetadata +} + +func TestDenomsMetadata(t *testing.T) { + t.Parallel() + f := initDeterministicFixture(t) + f.ctx = f.app.StateLatestContext(t) + gasMeterFactory := integration.GasMeterFactory(f.ctx) + queryFn := queryFnFactory[*banktypes.QueryDenomsMetadataRequest, *banktypes.QueryDenomsMetadataResponse](f) + + rapid.Check(t, func(rt *rapid.T) { + count := rapid.IntRange(1, 3).Draw(rt, "count") + denomsMetadata := createAndReturnMetadatas(rt, count) + require.True(t, len(denomsMetadata) == count) + + for i := 0; i < count; i++ { + f.bankKeeper.SetDenomMetaData(f.ctx, denomsMetadata[i]) + } + + req := &banktypes.QueryDenomsMetadataRequest{ + Pagination: testdata.PaginationGenerator(rt, uint64(count)).Draw(rt, "pagination"), + } + + testdata.DeterministicIterationsV2(t, req, gasMeterFactory, queryFn, assertNonZeroGas, nil) + }) + + require.NoError(t, f.app.Close()) + + f = initDeterministicFixture(t) // reset + f.ctx = f.app.StateLatestContext(t) + gasMeterFactory = integration.GasMeterFactory(f.ctx) + queryFn = queryFnFactory[*banktypes.QueryDenomsMetadataRequest, *banktypes.QueryDenomsMetadataResponse](f) + + f.bankKeeper.SetDenomMetaData(f.ctx, metadataAtom) + + req := &banktypes.QueryDenomsMetadataRequest{} + testdata.DeterministicIterationsV2(t, req, gasMeterFactory, queryFn, assertNonZeroGas, nil) +} + +func TestDenomMetadata(t *testing.T) { + t.Parallel() + f := initDeterministicFixture(t) + f.ctx = f.app.StateLatestContext(t) + gasMeterFactory := integration.GasMeterFactory(f.ctx) + queryFn := queryFnFactory[*banktypes.QueryDenomMetadataRequest, *banktypes.QueryDenomMetadataResponse](f) + + rapid.Check(t, func(rt *rapid.T) { + denomMetadata := createAndReturnMetadatas(rt, 1) + require.True(t, len(denomMetadata) == 1) + f.bankKeeper.SetDenomMetaData(f.ctx, denomMetadata[0]) + + req := &banktypes.QueryDenomMetadataRequest{ + Denom: denomMetadata[0].Base, + } + + testdata.DeterministicIterationsV2(t, req, gasMeterFactory, queryFn, assertNonZeroGas, nil) + }) + + f.bankKeeper.SetDenomMetaData(f.ctx, metadataAtom) + + req := &banktypes.QueryDenomMetadataRequest{ + Denom: metadataAtom.Base, + } + + testdata.DeterministicIterationsV2(t, req, gasMeterFactory, queryFn, assertNonZeroGas, nil) +} + +func TestSendEnabled(t *testing.T) { + t.Parallel() + f := initDeterministicFixture(t) + f.ctx = f.app.StateLatestContext(t) + gasMeterFactory := integration.GasMeterFactory(f.ctx) + queryFn := queryFnFactory[*banktypes.QuerySendEnabledRequest, *banktypes.QuerySendEnabledResponse](f) + allDenoms := []string{} + + rapid.Check(t, func(rt *rapid.T) { + count := rapid.IntRange(0, 10).Draw(rt, "count") + denoms := make([]string, 0, count) + + for i := 0; i < count; i++ { + coin := banktypes.SendEnabled{ + Denom: rapid.StringMatching(denomRegex).Draw(rt, "denom"), + Enabled: rapid.Bool().Draw(rt, "enabled-status"), + } + + f.bankKeeper.SetSendEnabled(f.ctx, coin.Denom, coin.Enabled) + denoms = append(denoms, coin.Denom) + } + + allDenoms = append(allDenoms, denoms...) + + req := &banktypes.QuerySendEnabledRequest{ + Denoms: denoms, + // Pagination is only taken into account when `denoms` is an empty array + Pagination: testdata.PaginationGenerator(rt, uint64(len(allDenoms))).Draw(rt, "pagination"), + } + testdata.DeterministicIterationsV2(t, req, gasMeterFactory, queryFn, assertNonZeroGas, nil) + }) + + coin1 := banktypes.SendEnabled{ + Denom: "falsecoin", + Enabled: false, + } + coin2 := banktypes.SendEnabled{ + Denom: "truecoin", + Enabled: true, + } + + f.bankKeeper.SetSendEnabled(f.ctx, coin1.Denom, false) + f.bankKeeper.SetSendEnabled(f.ctx, coin2.Denom, true) + + req := &banktypes.QuerySendEnabledRequest{ + Denoms: []string{coin1.GetDenom(), coin2.GetDenom()}, + } + + testdata.DeterministicIterationsV2(t, req, gasMeterFactory, queryFn, assertNonZeroGas, nil) +} + +func TestDenomOwners(t *testing.T) { + t.Parallel() + f := initDeterministicFixture(t) + f.ctx = f.app.StateLatestContext(t) + gasMeterFactory := integration.GasMeterFactory(f.ctx) + queryFn := queryFnFactory[*banktypes.QueryDenomOwnersRequest, *banktypes.QueryDenomOwnersResponse](f) + + rapid.Check(t, func(rt *rapid.T) { + denom := rapid.StringMatching(denomRegex).Draw(rt, "denom") + numAddr := rapid.IntRange(1, 10).Draw(rt, "number-address") + for i := 0; i < numAddr; i++ { + addr := testdata.AddressGenerator(rt).Draw(rt, "address") + + coin := sdk.NewCoin( + denom, + math.NewInt(rapid.Int64Min(1).Draw(rt, "amount")), + ) + + err := banktestutil.FundAccount(f.ctx, f.bankKeeper, addr, sdk.NewCoins(coin)) + require.NoError(t, err) + } + + req := &banktypes.QueryDenomOwnersRequest{ + Denom: denom, + Pagination: testdata.PaginationGenerator(rt, uint64(numAddr)).Draw(rt, "pagination"), + } + testdata.DeterministicIterationsV2(t, req, gasMeterFactory, queryFn, assertNonZeroGas, nil) + }) + + denomOwners := []*banktypes.DenomOwner{ + { + Address: "cosmos1qg65a9q6k2sqq7l3ycp428sqqpmqcucgzze299", + Balance: coin1, + }, + { + Address: "cosmos1qglnsqgpq48l7qqzgs8qdshr6fh3gqq9ej3qut", + Balance: coin1, + }, + } + + for i := 0; i < len(denomOwners); i++ { + addr, err := sdk.AccAddressFromBech32(denomOwners[i].Address) + require.NoError(t, err) + + err = banktestutil.FundAccount(f.ctx, f.bankKeeper, addr, sdk.NewCoins(coin1)) + require.NoError(t, err) + } + + req := &banktypes.QueryDenomOwnersRequest{ + Denom: coin1.GetDenom(), + } + testdata.DeterministicIterationsV2(t, req, gasMeterFactory, queryFn, assertNonZeroGas, nil) +} diff --git a/tests/integration/v2/genesis.go b/tests/integration/v2/genesis.go new file mode 100644 index 000000000000..d101ce3e8672 --- /dev/null +++ b/tests/integration/v2/genesis.go @@ -0,0 +1,182 @@ +package integration + +import ( + "encoding/json" + "errors" + "fmt" + "time" + + cmttypes "github.com/cometbft/cometbft/types" + + sdkmath "cosmossdk.io/math" + banktypes "cosmossdk.io/x/bank/types" + stakingtypes "cosmossdk.io/x/staking/types" + + "github.com/cosmos/cosmos-sdk/codec" + codectypes "github.com/cosmos/cosmos-sdk/codec/types" + cryptocodec "github.com/cosmos/cosmos-sdk/crypto/codec" + "github.com/cosmos/cosmos-sdk/testutil/mock" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/auth/tx" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" +) + +// genesisStateWithValSet returns a new genesis state with the validator set +func genesisStateWithValSet( + codec codec.Codec, + genesisState map[string]json.RawMessage, + valSet *cmttypes.ValidatorSet, + genAccs []authtypes.GenesisAccount, + balances ...banktypes.Balance, +) (map[string]json.RawMessage, error) { + if len(genAccs) == 0 { + return nil, errors.New("no genesis accounts provided") + } + // set genesis accounts + authGenesis := authtypes.NewGenesisState(authtypes.DefaultParams(), genAccs) + genesisState[authtypes.ModuleName] = codec.MustMarshalJSON(authGenesis) + + validators := make([]stakingtypes.Validator, 0, len(valSet.Validators)) + delegations := make([]stakingtypes.Delegation, 0, len(valSet.Validators)) + + bondAmt := sdk.DefaultPowerReduction + + for _, val := range valSet.Validators { + pk, err := cryptocodec.FromCmtPubKeyInterface(val.PubKey) + if err != nil { + return nil, fmt.Errorf("failed to convert pubkey: %w", err) + } + + pkAny, err := codectypes.NewAnyWithValue(pk) + if err != nil { + return nil, fmt.Errorf("failed to create new any: %w", err) + } + + validator := stakingtypes.Validator{ + OperatorAddress: sdk.ValAddress(val.Address).String(), + ConsensusPubkey: pkAny, + Jailed: false, + Status: stakingtypes.Bonded, + Tokens: bondAmt, + DelegatorShares: sdkmath.LegacyOneDec(), + Description: stakingtypes.Description{}, + UnbondingHeight: int64(0), + UnbondingTime: time.Unix(0, 0).UTC(), + Commission: stakingtypes.NewCommission( + sdkmath.LegacyZeroDec(), + sdkmath.LegacyZeroDec(), + sdkmath.LegacyZeroDec(), + ), + MinSelfDelegation: sdkmath.ZeroInt(), + } + validators = append(validators, validator) + delegations = append( + delegations, + stakingtypes.NewDelegation( + genAccs[0].GetAddress().String(), + sdk.ValAddress(val.Address).String(), + sdkmath.LegacyOneDec(), + ), + ) + + } + + // set validators and delegations + stakingGenesis := stakingtypes.NewGenesisState( + stakingtypes.DefaultParams(), + validators, + delegations, + ) + genesisState[stakingtypes.ModuleName] = codec.MustMarshalJSON( + stakingGenesis, + ) + + totalSupply := sdk.NewCoins() + for _, b := range balances { + // add genesis acc tokens to total supply + totalSupply = totalSupply.Add(b.Coins...) + } + + for range delegations { + // add delegated tokens to total supply + totalSupply = totalSupply.Add( + sdk.NewCoin(sdk.DefaultBondDenom, bondAmt), + ) + } + + // add bonded amount to bonded pool module account + balances = append(balances, banktypes.Balance{ + Address: authtypes.NewModuleAddress(stakingtypes.BondedPoolName). + String(), + Coins: sdk.Coins{sdk.NewCoin(sdk.DefaultBondDenom, bondAmt)}, + }) + + // update total supply + bankGenesis := banktypes.NewGenesisState( + banktypes.DefaultGenesisState().Params, + balances, + totalSupply, + []banktypes.Metadata{}, + []banktypes.SendEnabled{}, + ) + genesisState[banktypes.ModuleName] = codec.MustMarshalJSON(bankGenesis) + + return genesisState, nil +} + +// CreateRandomValidatorSet creates a validator set with one random validator +func CreateRandomValidatorSet() (*cmttypes.ValidatorSet, error) { + privVal := mock.NewPV() + pubKey, err := privVal.GetPubKey() + if err != nil { + return nil, fmt.Errorf("failed to get pub key: %w", err) + } + + // create validator set with single validator + validator := cmttypes.NewValidator(pubKey, 1) + + return cmttypes.NewValidatorSet([]*cmttypes.Validator{validator}), nil +} + +type GenesisAccount struct { + authtypes.GenesisAccount + Coins sdk.Coins +} + +type genesisTxCodec struct { + tx.ConfigOptions +} + +// Decode implements transaction.Codec. +func (t *genesisTxCodec) Decode(bz []byte) (stateMachineTx, error) { + var out stateMachineTx + tx, err := t.ProtoDecoder(bz) + if err != nil { + return out, err + } + + var ok bool + out, ok = tx.(stateMachineTx) + if !ok { + return out, errors.New("unexpected Tx type") + } + + return out, nil +} + +// DecodeJSON implements transaction.Codec. +func (t *genesisTxCodec) DecodeJSON(bz []byte) (stateMachineTx, error) { + var out stateMachineTx + tx, err := t.JSONDecoder(bz) + if err != nil { + return out, err + } + + var ok bool + out, ok = tx.(stateMachineTx) + if !ok { + return out, errors.New("unexpected Tx type") + } + + return out, nil +} diff --git a/tests/integration/v2/services.go b/tests/integration/v2/services.go new file mode 100644 index 000000000000..f69aa70574a0 --- /dev/null +++ b/tests/integration/v2/services.go @@ -0,0 +1,119 @@ +package integration + +import ( + "context" + "fmt" + + "cosmossdk.io/core/comet" + "cosmossdk.io/core/event" + "cosmossdk.io/core/gas" + "cosmossdk.io/core/server" + corestore "cosmossdk.io/core/store" + "cosmossdk.io/core/transaction" + stfgas "cosmossdk.io/server/v2/stf/gas" +) + +func (c cometServiceImpl) CometInfo(context.Context) comet.Info { + return comet.Info{} +} + +// Services + +var _ server.DynamicConfig = &dynamicConfigImpl{} + +type dynamicConfigImpl struct { + homeDir string +} + +func (d *dynamicConfigImpl) Get(key string) any { + return d.GetString(key) +} + +func (d *dynamicConfigImpl) GetString(key string) string { + switch key { + case "home": + return d.homeDir + case "store.app-db-backend": + return "goleveldb" + case "server.minimum-gas-prices": + return "0stake" + default: + panic(fmt.Sprintf("unknown key: %s", key)) + } +} + +func (d *dynamicConfigImpl) UnmarshalSub(string, any) (bool, error) { + return false, nil +} + +var _ comet.Service = &cometServiceImpl{} + +type cometServiceImpl struct{} + +type storeService struct { + actor []byte + executionService corestore.KVStoreService +} + +type contextKeyType struct{} + +var contextKey = contextKeyType{} + +type integrationContext struct { + state corestore.WriterMap + gasMeter gas.Meter +} + +func GasMeterFromContext(ctx context.Context) gas.Meter { + iCtx, ok := ctx.Value(contextKey).(*integrationContext) + if !ok { + return nil + } + return iCtx.gasMeter +} + +func GasMeterFactory(ctx context.Context) func() gas.Meter { + return func() gas.Meter { + return GasMeterFromContext(ctx) + } +} + +func (s storeService) OpenKVStore(ctx context.Context) corestore.KVStore { + const gasLimit = 100_000 + iCtx, ok := ctx.Value(contextKey).(*integrationContext) + if !ok { + return s.executionService.OpenKVStore(ctx) + } + + iCtx.gasMeter = stfgas.NewMeter(gasLimit) + writerMap := stfgas.NewMeteredWriterMap(stfgas.DefaultConfig, iCtx.gasMeter, iCtx.state) + state, err := writerMap.GetWriter(s.actor) + if err != nil { + panic(err) + } + return state +} + +var ( + _ event.Service = &eventService{} + _ event.Manager = &eventManager{} +) + +type eventService struct{} + +// EventManager implements event.Service. +func (e *eventService) EventManager(context.Context) event.Manager { + return &eventManager{} +} + +type eventManager struct{} + +// Emit implements event.Manager. +func (e *eventManager) Emit(event transaction.Msg) error { + return nil +} + +// EmitKV implements event.Manager. +func (e *eventManager) EmitKV(eventType string, attrs ...event.Attribute) error { + return nil +} diff --git a/testutil/configurator/configurator.go b/testutil/configurator/configurator.go index f6114e704f1f..98b248cb361b 100644 --- a/testutil/configurator/configurator.go +++ b/testutil/configurator/configurator.go @@ -3,6 +3,7 @@ package configurator import ( accountsmodulev1 "cosmossdk.io/api/cosmos/accounts/module/v1" runtimev1alpha1 "cosmossdk.io/api/cosmos/app/runtime/v1alpha1" + runtimev2 "cosmossdk.io/api/cosmos/app/runtime/v2" appv1alpha1 "cosmossdk.io/api/cosmos/app/v1alpha1" authmodulev1 "cosmossdk.io/api/cosmos/auth/module/v1" authzmodulev1 "cosmossdk.io/api/cosmos/authz/module/v1" @@ -440,3 +441,71 @@ func NewAppConfig(opts ...ModuleOption) depinject.Config { return appconfig.Compose(&appv1alpha1.Config{Modules: modules}) } + +func NewAppV2Config(opts ...ModuleOption) depinject.Config { + cfg := defaultConfig() + for _, opt := range opts { + opt(cfg) + } + + preBlockers := make([]string, 0) + beginBlockers := make([]string, 0) + endBlockers := make([]string, 0) + initGenesis := make([]string, 0) + overrides := make([]*runtimev2.StoreKeyConfig, 0) + + for _, s := range cfg.PreBlockersOrder { + if _, ok := cfg.ModuleConfigs[s]; ok { + preBlockers = append(preBlockers, s) + } + } + + for _, s := range cfg.BeginBlockersOrder { + if _, ok := cfg.ModuleConfigs[s]; ok { + beginBlockers = append(beginBlockers, s) + } + } + + for _, s := range cfg.EndBlockersOrder { + if _, ok := cfg.ModuleConfigs[s]; ok { + endBlockers = append(endBlockers, s) + } + } + + for _, s := range cfg.InitGenesisOrder { + if _, ok := cfg.ModuleConfigs[s]; ok { + initGenesis = append(initGenesis, s) + } + } + + if _, ok := cfg.ModuleConfigs[testutil.AuthModuleName]; ok { + overrides = append(overrides, &runtimev2.StoreKeyConfig{ModuleName: testutil.AuthModuleName, KvStoreKey: "acc"}) + } + + runtimeConfig := &runtimev2.Module{ + AppName: "TestApp", + PreBlockers: preBlockers, + BeginBlockers: beginBlockers, + EndBlockers: endBlockers, + OverrideStoreKeys: overrides, + GasConfig: &runtimev2.GasConfig{ + ValidateTxGasLimit: 100_000, + QueryGasLimit: 100_000, + SimulationGasLimit: 100_000, + }, + } + if cfg.setInitGenesis { + runtimeConfig.InitGenesis = initGenesis + } + + modules := []*appv1alpha1.ModuleConfig{{ + Name: "runtime", + Config: appconfig.WrapAny(runtimeConfig), + }} + + for _, m := range cfg.ModuleConfigs { + modules = append(modules, m) + } + + return appconfig.Compose(&appv1alpha1.Config{Modules: modules}) +} diff --git a/testutil/testdata/grpc_query.go b/testutil/testdata/grpc_query.go index a8b4d75b0621..beda71db44c9 100644 --- a/testutil/testdata/grpc_query.go +++ b/testutil/testdata/grpc_query.go @@ -6,13 +6,15 @@ import ( "fmt" "testing" + "github.com/cosmos/gogoproto/proto" gogoprotoany "github.com/cosmos/gogoproto/types/any" "github.com/cosmos/gogoproto/types/any/test" - - "github.com/cosmos/gogoproto/proto" + "github.com/stretchr/testify/require" "google.golang.org/grpc" "gotest.tools/v3/assert" + "cosmossdk.io/core/gas" + "github.com/cosmos/cosmos-sdk/codec/types" sdk "github.com/cosmos/cosmos-sdk/types" ) @@ -85,6 +87,7 @@ func DeterministicIterations[request, response proto.Message]( if gasOverwrite { // to handle regressions, i.e. check that gas consumption didn't change gasConsumed = ctx.GasMeter().GasConsumed() - before } + t.Logf("gas consumed: %d", gasConsumed) for i := 0; i < iterCount; i++ { before := ctx.GasMeter().GasConsumed() @@ -94,3 +97,30 @@ func DeterministicIterations[request, response proto.Message]( assert.DeepEqual(t, res, prevRes) } } + +func DeterministicIterationsV2[request, response proto.Message]( + t *testing.T, + req request, + meterFn func() gas.Meter, + queryFn func(request) (response, error), + assertGas func(*testing.T, gas.Gas), + assertResponse func(*testing.T, response), +) { + t.Helper() + prevRes, err := queryFn(req) + gasMeter := meterFn() + gasConsumed := gasMeter.Consumed() + require.NoError(t, err) + assertGas(t, gasConsumed) + + for i := 0; i < iterCount; i++ { + res, err := queryFn(req) + require.NoError(t, err) + sameGas := gasMeter.Consumed() + require.Equal(t, gasConsumed, sameGas) + require.Equal(t, res, prevRes) + if assertResponse != nil { + assertResponse(t, res) + } + } +}