From 0f8a4c20d0c54ad9b89b253c55a35a9951477d17 Mon Sep 17 00:00:00 2001 From: Tanmay Date: Tue, 22 Oct 2024 09:56:31 -0400 Subject: [PATCH] test: add simulation tests (#2947) * debug sim test * start modifuing for v50 * add sim test v1 * add sim export * move simulation tests to a separate directory * remove unused functions * add comments * enable gov module * enable gov module * fix lint error * add todos for BasicManager * add todos for BasicManager * add cleanup for TestFullAppSimulation * register legacy router * added a doc for simulation testing * undo db close for multi threaded test * add description for tests * remove go module from simulation tests * make basicsmanager private * Update Makefile Co-authored-by: Francisco de Borja Aranda Castillejo * Update changelog.md Co-authored-by: Francisco de Borja Aranda Castillejo * format simulation.md --------- Co-authored-by: Francisco de Borja Aranda Castillejo --- Makefile | 55 +++++ app/app.go | 103 ++++----- app/encoding.go | 2 + app/genesis.go | 21 -- app/init_genesis.go | 66 ------ app/modules.go | 182 +++++++++++++++ changelog.md | 1 + docs/development/SIMULATION_TESTING.md | 34 +++ e2e/runner/admin_evm.go | 6 +- e2e/runner/report.go | 2 +- readme.md | 1 + tests/simulation/sim/sim_config.go | 132 +++++++++++ tests/simulation/sim/sim_state.go | 297 +++++++++++++++++++++++++ tests/simulation/sim/sim_utils.go | 68 ++++++ tests/simulation/sim_test.go | 213 ++++++++++++++++++ testutil/simapp/default_constants.go | 25 --- testutil/simapp/simapp.go | 219 ------------------ 17 files changed, 1032 insertions(+), 395 deletions(-) delete mode 100644 app/genesis.go delete mode 100644 app/init_genesis.go create mode 100644 app/modules.go create mode 100644 docs/development/SIMULATION_TESTING.md create mode 100644 tests/simulation/sim/sim_config.go create mode 100644 tests/simulation/sim/sim_state.go create mode 100644 tests/simulation/sim/sim_utils.go create mode 100644 tests/simulation/sim_test.go delete mode 100644 testutil/simapp/default_constants.go delete mode 100644 testutil/simapp/simapp.go diff --git a/Makefile b/Makefile index 7b01eafb41..f1e56c2953 100644 --- a/Makefile +++ b/Makefile @@ -354,6 +354,61 @@ start-upgrade-import-mainnet-test: zetanode-upgrade export UPGRADE_HEIGHT=225 && \ cd contrib/localnet/ && ./scripts/import-data.sh mainnet && $(DOCKER_COMPOSE) --profile upgrade -f docker-compose.yml -f docker-compose-upgrade.yml up -d + +############################################################################### +### Simulation Tests ### +############################################################################### + +BINDIR ?= $(GOPATH)/bin +SIMAPP = ./tests/simulation + + +# Run sim is a cosmos tool which helps us to run multiple simulations in parallel. +runsim: $(BINDIR)/runsim +$(BINDIR)/runsim: + @echo 'Installing runsim...' + @TEMP_DIR=$$(mktemp -d) && \ + cd $$TEMP_DIR && \ + go install github.com/cosmos/tools/cmd/runsim@v1.0.0 && \ + rm -rf $$TEMP_DIR || (echo 'Failed to install runsim' && exit 1) + @echo 'runsim installed successfully' + + +# Configuration parameters for simulation tests +# NumBlocks: Number of blocks to simulate +# BlockSize: Number of transactions in a block +# Commit: Whether to commit the block or not +# Period: Invariant check period +# Timeout: Timeout for the simulation test +define run-sim-test + @echo "Running $(1)..." + @go test -mod=readonly $(SIMAPP) -run $(2) -Enabled=true \ + -NumBlocks=$(3) -BlockSize=$(4) -Commit=true -Period=0 -v -timeout $(5) +endef + +test-sim-nondeterminism: + $(call run-sim-test,"non-determinism test",TestAppStateDeterminism,100,200,2h) + +test-sim-fullappsimulation: + $(call run-sim-test,"TestFullAppSimulation",TestFullAppSimulation,100,200,2h) + +test-sim-multi-seed-long: runsim + @echo "Running long multi-seed application simulation." + @$(BINDIR)/runsim -Jobs=4 -SimAppPkg=$(SIMAPP) -ExitOnFail 500 50 TestFullAppSimulation + +test-sim-multi-seed-short: runsim + @echo "Running short multi-seed application simulation." + @$(BINDIR)/runsim -Jobs=4 -SimAppPkg=$(SIMAPP) -ExitOnFail 50 10 TestFullAppSimulation + + + +.PHONY: \ +test-sim-nondeterminism \ +test-sim-fullappsimulation \ +test-sim-multi-seed-long \ +test-sim-multi-seed-short + + ############################################################################### ### GoReleaser ### ############################################################################### diff --git a/app/app.go b/app/app.go index 897cf305fe..1e64321f21 100644 --- a/app/app.go +++ b/app/app.go @@ -1,6 +1,7 @@ package app import ( + "encoding/json" "io" "net/http" "os" @@ -178,64 +179,33 @@ func getGovProposalHandlers() []govclient.ProposalHandler { return govProposalHandlers } -var ( - ModuleBasics = module.NewBasicManager( - auth.AppModuleBasic{}, - genutil.NewAppModuleBasic(genutiltypes.DefaultMessageValidator), - bank.AppModuleBasic{}, - //capability.AppModuleBasic{}, - staking.AppModuleBasic{}, - distr.AppModuleBasic{}, - gov.NewAppModuleBasic(getGovProposalHandlers()), - params.AppModuleBasic{}, - crisis.AppModuleBasic{}, - slashing.AppModuleBasic{}, - //ibc.AppModuleBasic{}, - //ibctm.AppModuleBasic{}, - upgrade.AppModuleBasic{}, - evidence.AppModuleBasic{}, - //transfer.AppModuleBasic{}, - vesting.AppModuleBasic{}, - consensus.AppModuleBasic{}, - evm.AppModuleBasic{}, - feemarket.AppModuleBasic{}, - authoritymodule.AppModuleBasic{}, - lightclientmodule.AppModuleBasic{}, - crosschainmodule.AppModuleBasic{}, - //ibccrosschain.AppModuleBasic{}, - observermodule.AppModuleBasic{}, - fungiblemodule.AppModuleBasic{}, - emissionsmodule.AppModuleBasic{}, - groupmodule.AppModuleBasic{}, - authzmodule.AppModuleBasic{}, - ) - - // module account permissions - maccPerms = map[string][]string{ - authtypes.FeeCollectorName: nil, - distrtypes.ModuleName: nil, - stakingtypes.BondedPoolName: {authtypes.Burner, authtypes.Staking}, - stakingtypes.NotBondedPoolName: {authtypes.Burner, authtypes.Staking}, - govtypes.ModuleName: {authtypes.Burner}, - //ibctransfertypes.ModuleName: {authtypes.Minter, authtypes.Burner}, - crosschaintypes.ModuleName: {authtypes.Minter, authtypes.Burner}, - //ibccrosschaintypes.ModuleName: nil, - evmtypes.ModuleName: {authtypes.Minter, authtypes.Burner}, - fungibletypes.ModuleName: {authtypes.Minter, authtypes.Burner}, - emissionstypes.ModuleName: nil, - emissionstypes.UndistributedObserverRewardsPool: nil, - emissionstypes.UndistributedTSSRewardsPool: nil, - } +type GenesisState map[string]json.RawMessage + +// module account permissions +var maccPerms = map[string][]string{ + authtypes.FeeCollectorName: nil, + distrtypes.ModuleName: nil, + stakingtypes.BondedPoolName: {authtypes.Burner, authtypes.Staking}, + stakingtypes.NotBondedPoolName: {authtypes.Burner, authtypes.Staking}, + govtypes.ModuleName: {authtypes.Burner}, + //ibctransfertypes.ModuleName: {authtypes.Minter, authtypes.Burner}, + crosschaintypes.ModuleName: {authtypes.Minter, authtypes.Burner}, + //ibccrosschaintypes.ModuleName: nil, + evmtypes.ModuleName: {authtypes.Minter, authtypes.Burner}, + fungibletypes.ModuleName: {authtypes.Minter, authtypes.Burner}, + emissionstypes.ModuleName: nil, + emissionstypes.UndistributedObserverRewardsPool: nil, + emissionstypes.UndistributedTSSRewardsPool: nil, +} - // module accounts that are NOT allowed to receive tokens - blockedReceivingModAcc = map[string]bool{ - distrtypes.ModuleName: true, - authtypes.FeeCollectorName: true, - stakingtypes.BondedPoolName: true, - stakingtypes.NotBondedPoolName: true, - govtypes.ModuleName: true, - } -) +// module accounts that are NOT allowed to receive tokens +var blockedReceivingModAcc = map[string]bool{ + distrtypes.ModuleName: true, + authtypes.FeeCollectorName: true, + stakingtypes.BondedPoolName: true, + stakingtypes.NotBondedPoolName: true, + govtypes.ModuleName: true, +} var ( _ runtime.AppI = (*App)(nil) @@ -260,6 +230,7 @@ type App struct { mm *module.Manager sm *module.SimulationManager + mb module.BasicManager configurator module.Configurator // sdk keepers @@ -637,6 +608,7 @@ func New( govRouter.AddRoute(govtypes.RouterKey, govv1beta1.ProposalHandler). AddRoute(paramproposal.RouterKey, params.NewParamChangeProposalHandler(app.ParamsKeeper)). AddRoute(upgradetypes.RouterKey, upgrade.NewSoftwareUpgradeProposalHandler(app.UpgradeKeeper)) + govConfig := govtypes.DefaultConfig() govKeeper := govkeeper.NewKeeper( appCodec, @@ -648,11 +620,14 @@ func New( govConfig, authAddr, ) + app.GovKeeper = *govKeeper.SetHooks( govtypes.NewMultiGovHooks( // register governance hooks ), ) + // Set legacy router for backwards compatibility with gov v1beta1 + // app.GovKeeper.SetLegacyRouter(govRouter) // Create evidence Keeper for to register the IBC light client misbehaviour evidence route evidenceKeeper := evidencekeeper.NewKeeper( @@ -738,6 +713,8 @@ func New( authzmodule.NewAppModule(appCodec, app.AuthzKeeper, app.AccountKeeper, app.BankKeeper, app.interfaceRegistry), ) + app.mb = ModuleBasics + // During begin block slashing happens after distr.BeginBlocker so that // there is nothing left over in the validator fee pool, so as to keep the // CanWithdrawInvariant invariant. @@ -813,6 +790,10 @@ func New( app.configurator = module.NewConfigurator(app.appCodec, app.MsgServiceRouter(), app.GRPCQueryRouter()) app.mm.RegisterServices(app.configurator) + app.sm = module.NewSimulationManager(simulationModules(app, appCodec, skipGenesisInvariants)...) + + app.sm.RegisterStoreDecoders() + // initialize stores app.MountKVStores(keys) app.MountTransientStores(tKeys) @@ -920,7 +901,7 @@ func (app *App) ModuleAccountAddrs() map[string]bool { return modAccAddrs } -// LegacyAmino returns SimApp's amino codec. +// LegacyAmino returns app's amino codec. // // NOTE: This is solely to be used for testing purposes as it may be desirable // for modules to register their own custom testing types. @@ -983,7 +964,7 @@ func (app *App) RegisterAPIRoutes(apiSvr *api.Server, apiConfig config.APIConfig nodeservice.RegisterGRPCGatewayRoutes(clientCtx, apiSvr.GRPCGatewayRouter) // Register legacy and grpc-gateway routes for all modules. - ModuleBasics.RegisterGRPCGatewayRoutes(clientCtx, apiSvr.GRPCGatewayRouter) + app.mb.RegisterGRPCGatewayRoutes(clientCtx, apiSvr.GRPCGatewayRouter) // register app's OpenAPI routes. if apiConfig.Swagger { @@ -1075,6 +1056,10 @@ func (app *App) SimulationManager() *module.SimulationManager { return app.sm } +func (app *App) BasicManager() module.BasicManager { + return app.mb +} + func (app *App) BlockedAddrs() map[string]bool { blockList := make(map[string]bool) diff --git a/app/encoding.go b/app/encoding.go index 2e8c728b0f..674887f670 100644 --- a/app/encoding.go +++ b/app/encoding.go @@ -28,6 +28,8 @@ func MakeEncodingConfig() ethermint.EncodingConfig { encodingConfig := evmenc.MakeConfig(ModuleBasics) registry := encodingConfig.InterfaceRegistry + // TODO test if we need to register these interfaces again as MakeConfig already registers them + // https://github.com/zeta-chain/node/issues/3003 cryptocodec.RegisterInterfaces(registry) authtypes.RegisterInterfaces(registry) authz.RegisterInterfaces(registry) diff --git a/app/genesis.go b/app/genesis.go deleted file mode 100644 index 5bf0c1da80..0000000000 --- a/app/genesis.go +++ /dev/null @@ -1,21 +0,0 @@ -package app - -import ( - "encoding/json" - - "github.com/cosmos/cosmos-sdk/codec" -) - -// The genesis state of the blockchain is represented here as a map of raw json -// messages key'd by a identifier string. -// The identifier is used to determine which module genesis information belongs -// to so it may be appropriately routed during init chain. -// Within this application default genesis information is retrieved from -// the ModuleBasicManager which populates json from each BasicModule -// object provided to it during init. -type GenesisState map[string]json.RawMessage - -// NewDefaultGenesisState generates the default state for the application. -func NewDefaultGenesisState(cdc codec.JSONCodec) GenesisState { - return ModuleBasics.DefaultGenesis(cdc) -} diff --git a/app/init_genesis.go b/app/init_genesis.go deleted file mode 100644 index b69972dbf8..0000000000 --- a/app/init_genesis.go +++ /dev/null @@ -1,66 +0,0 @@ -package app - -import ( - authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" - vestingtypes "github.com/cosmos/cosmos-sdk/x/auth/vesting/types" - "github.com/cosmos/cosmos-sdk/x/authz" - banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" - consensusparamtypes "github.com/cosmos/cosmos-sdk/x/consensus/types" - crisistypes "github.com/cosmos/cosmos-sdk/x/crisis/types" - distrtypes "github.com/cosmos/cosmos-sdk/x/distribution/types" - evidencetypes "github.com/cosmos/cosmos-sdk/x/evidence/types" - genutiltypes "github.com/cosmos/cosmos-sdk/x/genutil/types" - govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" - "github.com/cosmos/cosmos-sdk/x/group" - paramstypes "github.com/cosmos/cosmos-sdk/x/params/types" - slashingtypes "github.com/cosmos/cosmos-sdk/x/slashing/types" - stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" - upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" - evmtypes "github.com/zeta-chain/ethermint/x/evm/types" - feemarkettypes "github.com/zeta-chain/ethermint/x/feemarket/types" - - authoritytypes "github.com/zeta-chain/node/x/authority/types" - crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" - emissionsModuleTypes "github.com/zeta-chain/node/x/emissions/types" - fungibleModuleTypes "github.com/zeta-chain/node/x/fungible/types" - lightclienttypes "github.com/zeta-chain/node/x/lightclient/types" - observertypes "github.com/zeta-chain/node/x/observer/types" -) - -// InitGenesisModuleList returns the module list for genesis initialization -// NOTE: Capability module must occur first so that it can initialize any capabilities -// TODO: enable back IBC -// all commented lines in this function are modules related to IBC -// https://github.com/zeta-chain/node/issues/2573 -func InitGenesisModuleList() []string { - return []string{ - //capabilitytypes.ModuleName, - authtypes.ModuleName, - banktypes.ModuleName, - distrtypes.ModuleName, - stakingtypes.ModuleName, - slashingtypes.ModuleName, - govtypes.ModuleName, - //ibcexported.ModuleName, - //ibctransfertypes.ModuleName, - evmtypes.ModuleName, - feemarkettypes.ModuleName, - paramstypes.ModuleName, - group.ModuleName, - genutiltypes.ModuleName, - upgradetypes.ModuleName, - evidencetypes.ModuleName, - vestingtypes.ModuleName, - observertypes.ModuleName, - crosschaintypes.ModuleName, - //ibccrosschaintypes.ModuleName, - fungibleModuleTypes.ModuleName, - emissionsModuleTypes.ModuleName, - authz.ModuleName, - authoritytypes.ModuleName, - lightclienttypes.ModuleName, - consensusparamtypes.ModuleName, - // NOTE: crisis module must go at the end to check for invariants on each module - crisistypes.ModuleName, - } -} diff --git a/app/modules.go b/app/modules.go new file mode 100644 index 0000000000..6f164a9409 --- /dev/null +++ b/app/modules.go @@ -0,0 +1,182 @@ +package app + +import ( + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/types/module" + "github.com/cosmos/cosmos-sdk/x/auth" + authsims "github.com/cosmos/cosmos-sdk/x/auth/simulation" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + "github.com/cosmos/cosmos-sdk/x/auth/vesting" + vestingtypes "github.com/cosmos/cosmos-sdk/x/auth/vesting/types" + "github.com/cosmos/cosmos-sdk/x/authz" + authzmodule "github.com/cosmos/cosmos-sdk/x/authz/module" + "github.com/cosmos/cosmos-sdk/x/bank" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + "github.com/cosmos/cosmos-sdk/x/consensus" + consensusparamtypes "github.com/cosmos/cosmos-sdk/x/consensus/types" + "github.com/cosmos/cosmos-sdk/x/crisis" + crisistypes "github.com/cosmos/cosmos-sdk/x/crisis/types" + distr "github.com/cosmos/cosmos-sdk/x/distribution" + distrtypes "github.com/cosmos/cosmos-sdk/x/distribution/types" + "github.com/cosmos/cosmos-sdk/x/evidence" + evidencetypes "github.com/cosmos/cosmos-sdk/x/evidence/types" + "github.com/cosmos/cosmos-sdk/x/genutil" + genutiltypes "github.com/cosmos/cosmos-sdk/x/genutil/types" + "github.com/cosmos/cosmos-sdk/x/gov" + govtypes "github.com/cosmos/cosmos-sdk/x/gov/types" + "github.com/cosmos/cosmos-sdk/x/group" + groupmodule "github.com/cosmos/cosmos-sdk/x/group/module" + "github.com/cosmos/cosmos-sdk/x/params" + paramstypes "github.com/cosmos/cosmos-sdk/x/params/types" + "github.com/cosmos/cosmos-sdk/x/slashing" + slashingtypes "github.com/cosmos/cosmos-sdk/x/slashing/types" + "github.com/cosmos/cosmos-sdk/x/staking" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + "github.com/cosmos/cosmos-sdk/x/upgrade" + upgradetypes "github.com/cosmos/cosmos-sdk/x/upgrade/types" + "github.com/zeta-chain/ethermint/x/evm" + evmtypes "github.com/zeta-chain/ethermint/x/evm/types" + "github.com/zeta-chain/ethermint/x/feemarket" + feemarkettypes "github.com/zeta-chain/ethermint/x/feemarket/types" + + authoritymodule "github.com/zeta-chain/node/x/authority" + authoritytypes "github.com/zeta-chain/node/x/authority/types" + crosschainmodule "github.com/zeta-chain/node/x/crosschain" + crosschaintypes "github.com/zeta-chain/node/x/crosschain/types" + emissionsmodule "github.com/zeta-chain/node/x/emissions" + emissionsModuleTypes "github.com/zeta-chain/node/x/emissions/types" + fungiblemodule "github.com/zeta-chain/node/x/fungible" + fungibleModuleTypes "github.com/zeta-chain/node/x/fungible/types" + lightclientmodule "github.com/zeta-chain/node/x/lightclient" + lightclienttypes "github.com/zeta-chain/node/x/lightclient/types" + observermodule "github.com/zeta-chain/node/x/observer" + observertypes "github.com/zeta-chain/node/x/observer/types" +) + +// ModuleBasics defines the module BasicManager that is in charge of setting up basic, +// non-dependant module elements, such as codec registration +// and genesis verification. +// https://github.com/zeta-chain/node/issues/3021 +// TODO: Use app.mm to create the basic manager instead +var ModuleBasics = module.NewBasicManager( + auth.AppModuleBasic{}, + genutil.NewAppModuleBasic(genutiltypes.DefaultMessageValidator), + bank.AppModuleBasic{}, + //capability.AppModuleBasic{}, + staking.AppModuleBasic{}, + distr.AppModuleBasic{}, + gov.NewAppModuleBasic(getGovProposalHandlers()), + params.AppModuleBasic{}, + crisis.AppModuleBasic{}, + slashing.AppModuleBasic{}, + //ibc.AppModuleBasic{}, + //ibctm.AppModuleBasic{}, + upgrade.AppModuleBasic{}, + evidence.AppModuleBasic{}, + //transfer.AppModuleBasic{}, + vesting.AppModuleBasic{}, + consensus.AppModuleBasic{}, + evm.AppModuleBasic{}, + feemarket.AppModuleBasic{}, + authoritymodule.AppModuleBasic{}, + lightclientmodule.AppModuleBasic{}, + crosschainmodule.AppModuleBasic{}, + //ibccrosschain.AppModuleBasic{}, + observermodule.AppModuleBasic{}, + fungiblemodule.AppModuleBasic{}, + emissionsmodule.AppModuleBasic{}, + groupmodule.AppModuleBasic{}, + authzmodule.AppModuleBasic{}, +) + +// InitGenesisModuleList returns the module list for genesis initialization +// NOTE: Capability module must occur first so that it can initialize any capabilities +// TODO: enable back IBC +// all commented lines in this function are modules related to IBC +// https://github.com/zeta-chain/node/issues/2573 +func InitGenesisModuleList() []string { + return []string{ + //capabilitytypes.ModuleName, + authtypes.ModuleName, + banktypes.ModuleName, + distrtypes.ModuleName, + stakingtypes.ModuleName, + slashingtypes.ModuleName, + govtypes.ModuleName, + //ibcexported.ModuleName, + //ibctransfertypes.ModuleName, + evmtypes.ModuleName, + feemarkettypes.ModuleName, + paramstypes.ModuleName, + group.ModuleName, + genutiltypes.ModuleName, + upgradetypes.ModuleName, + evidencetypes.ModuleName, + vestingtypes.ModuleName, + observertypes.ModuleName, + crosschaintypes.ModuleName, + //ibccrosschaintypes.ModuleName, + fungibleModuleTypes.ModuleName, + emissionsModuleTypes.ModuleName, + authz.ModuleName, + authoritytypes.ModuleName, + lightclienttypes.ModuleName, + consensusparamtypes.ModuleName, + // NOTE: crisis module must go at the end to check for invariants on each module + crisistypes.ModuleName, + } +} + +// simulationModules returns a list of modules to include in the simulation +func simulationModules( + app *App, + appCodec codec.Codec, + _ bool, +) []module.AppModuleSimulation { + return []module.AppModuleSimulation{ + auth.NewAppModule( + appCodec, + app.AccountKeeper, + authsims.RandomGenesisAccounts, + app.GetSubspace(authtypes.ModuleName), + ), + bank.NewAppModule(appCodec, app.BankKeeper, app.AccountKeeper, app.GetSubspace(banktypes.ModuleName)), + // Todo : Enable gov module simulation + // https://github.com/zeta-chain/node/issues/3007 + //gov.NewAppModule( + // appCodec, + // &app.GovKeeper, + // app.AccountKeeper, + // app.BankKeeper, + // app.GetSubspace(govtypes.ModuleName), + //), + staking.NewAppModule( + appCodec, + app.StakingKeeper, + app.AccountKeeper, + app.BankKeeper, + app.GetSubspace(stakingtypes.ModuleName), + ), + distr.NewAppModule( + appCodec, + app.DistrKeeper, + app.AccountKeeper, + app.BankKeeper, + app.StakingKeeper, + app.GetSubspace(distrtypes.ModuleName), + ), + slashing.NewAppModule( + appCodec, + app.SlashingKeeper, + app.AccountKeeper, + app.BankKeeper, + app.StakingKeeper, + app.GetSubspace(slashingtypes.ModuleName), + ), + params.NewAppModule(app.ParamsKeeper), + evidence.NewAppModule(app.EvidenceKeeper), + evm.NewAppModule(app.EvmKeeper, app.AccountKeeper, app.GetSubspace(evmtypes.ModuleName)), + authzmodule.NewAppModule(appCodec, app.AuthzKeeper, app.AccountKeeper, app.BankKeeper, app.interfaceRegistry), + groupmodule.NewAppModule(appCodec, app.GroupKeeper, app.AccountKeeper, app.BankKeeper, app.interfaceRegistry), + } +} diff --git a/changelog.md b/changelog.md index 56ccf83d84..b666c26f7d 100644 --- a/changelog.md +++ b/changelog.md @@ -43,6 +43,7 @@ * [2895](https://github.com/zeta-chain/node/pull/2895) - add e2e test for bitcoin deposit and call * [2894](https://github.com/zeta-chain/node/pull/2894) - increase gas limit for TSS vote tx * [2932](https://github.com/zeta-chain/node/pull/2932) - add gateway upgrade as part of the upgrade test +* [2947](https://github.com/zeta-chain/node/pull/2947) - initialize simulation tests ### Fixes diff --git a/docs/development/SIMULATION_TESTING.md b/docs/development/SIMULATION_TESTING.md new file mode 100644 index 0000000000..1f5232911a --- /dev/null +++ b/docs/development/SIMULATION_TESTING.md @@ -0,0 +1,34 @@ +# Zetachain simulation testing +## Overview +The blockchain simulation tests how the blockchain application would behave under real life circumstances by generating +and sending randomized messages.The goal of this is to detect and debug failures that could halt a live chain,by providing +logs and statistics about the operations run by the simulator as well as exporting the latest application state. + + +## Simulation tests + +### Nondeterminism test +Nondeterminism test runs a full application simulation , and produces multiple blocks as per the config +It checks the determinism of the application by comparing the apphash at the end of each run to other runs +The test certifies that , for the same set of operations ( irrespective of what the operations are ), we would reach the same final state if the initial state is the same +```bash +make test-sim-nondeterminism +``` +### Full application simulation test +Full application runs a full app simulation test with the provided configuration. +At the end of the run it tries to export the genesis state to make sure the export works. +```bash +make test-sim-full-app +``` + +### Multi seed long test +Multi seed long test runs a full application simulation with multiple seeds and multiple blocks.This runs the test for a longer duration compared to the multi seed short test +```bash +make test-sim-multi-seed-long +``` + +### Multi seed short test +Multi seed short test runs a full application simulation with multiple seeds and multiple blocks. This runs the test for a longer duration compared to the multi seed long test +```bash +make test-sim-multi-seed-short +``` \ No newline at end of file diff --git a/e2e/runner/admin_evm.go b/e2e/runner/admin_evm.go index 1cd0c29db8..88b0a1121d 100644 --- a/e2e/runner/admin_evm.go +++ b/e2e/runner/admin_evm.go @@ -1,8 +1,6 @@ package runner import ( - "fmt" - "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/stretchr/testify/require" @@ -14,7 +12,7 @@ func (r *E2ERunner) UpdateTssAddressForConnector() { tx, err := r.ConnectorEth.UpdateTssAddress(r.EVMAuth, r.TSSAddress) require.NoError(r, err) - r.Logger.Info(fmt.Sprintf("TSS Address Update Tx: %s", tx.Hash().String())) + r.Logger.Info("TSS Address Update Tx: %s", tx.Hash().String()) receipt := utils.MustWaitForTxReceipt(r.Ctx, r.EVMClient, tx, r.Logger, r.ReceiptTimeout) utils.RequireTxSuccessful(r, receipt) @@ -28,7 +26,7 @@ func (r *E2ERunner) UpdateTssAddressForErc20custody() { tx, err := r.ERC20CustodyV2.UpdateTSSAddress(r.EVMAuth, r.TSSAddress) require.NoError(r, err) - r.Logger.Info(fmt.Sprintf("TSS ERC20 Address Update Tx: %s", tx.Hash().String())) + r.Logger.Info("TSS ERC20 Address Update Tx: %s", tx.Hash().String()) receipt := utils.MustWaitForTxReceipt(r.Ctx, r.EVMClient, tx, r.Logger, r.ReceiptTimeout) utils.RequireTxSuccessful(r, receipt) diff --git a/e2e/runner/report.go b/e2e/runner/report.go index c860f014b1..b36e3033bc 100644 --- a/e2e/runner/report.go +++ b/e2e/runner/report.go @@ -59,7 +59,7 @@ func (r *E2ERunner) PrintTestReports(tr TestReports) { if err != nil { r.Logger.Print("Error rendering test report: %s", err) } - r.Logger.PrintNoPrefix(table) + r.Logger.PrintNoPrefix("%s", table) } // NetworkReport is a struct that contains the report for the network used after running e2e tests diff --git a/readme.md b/readme.md index 6995b2c6a3..5e7230fe00 100644 --- a/readme.md +++ b/readme.md @@ -83,6 +83,7 @@ Find below further documentation for development and running your own ZetaChain - [Run the E2E tests and interact with the localnet](docs/development/LOCAL_TESTING.md) - [Make a new ZetaChain release](docs/development/RELEASES.md) - [Deploy your own ZetaChain or Bitcoin node](docs/development/DEPLOY_NODES.md) +- [Run the simulation tests](docs/development/SIMULATION_TESTING.md) ## Community diff --git a/tests/simulation/sim/sim_config.go b/tests/simulation/sim/sim_config.go new file mode 100644 index 0000000000..8a6c281db8 --- /dev/null +++ b/tests/simulation/sim/sim_config.go @@ -0,0 +1,132 @@ +package sim + +import ( + "flag" + + "github.com/cosmos/cosmos-sdk/types/simulation" +) + +const ( + defaultSeed = 42 + defaultNumBlocks = 500 + defaultInitialHeight = 1 + defaultBlockSize = 200 +) + +// List of available flags for the simulator +var ( + FlagGenesisFileValue string + FlagParamsFileValue string + FlagExportParamsPathValue string + FlagExportParamsHeightValue int + FlagExportStatePathValue string + FlagExportStatsPathValue string + FlagSeedValue int64 + FlagInitialBlockHeightValue int + FlagNumBlocksValue int + FlagBlockSizeValue int + FlagLeanValue bool + FlagCommitValue bool + FlagOnOperationValue bool + FlagAllInvariantsValue bool + + FlagEnabledValue bool + FlagVerboseValue bool + FlagPeriodValue uint + FlagGenesisTimeValue int64 +) + +// GetSimulatorFlags gets the values of all the available simulation flags +func GetSimulatorFlags() { + // config fields + flag.StringVar( + &FlagGenesisFileValue, + "Genesis", + "", + "custom simulation genesis file; cannot be used with params file", + ) + flag.StringVar( + &FlagParamsFileValue, + "Params", + "", + "custom simulation params file which overrides any random params; cannot be used with genesis", + ) + flag.StringVar( + &FlagExportParamsPathValue, + "ExportParamsPath", + "", + "custom file path to save the exported params JSON", + ) + flag.IntVar( + &FlagExportParamsHeightValue, + "ExportParamsHeight", + 0, + "height to which export the randomly generated params", + ) + flag.StringVar( + &FlagExportStatePathValue, + "ExportStatePath", + "", + "custom file path to save the exported app state JSON", + ) + flag.StringVar( + &FlagExportStatsPathValue, + "ExportStatsPath", + "", + "custom file path to save the exported simulation statistics JSON", + ) + flag.Int64Var(&FlagSeedValue, "Seed", defaultSeed, "simulation random seed") + flag.IntVar( + &FlagInitialBlockHeightValue, + "InitialBlockHeight", + defaultInitialHeight, + "initial block to start the simulation", + ) + flag.IntVar( + &FlagNumBlocksValue, + "NumBlocks", + defaultNumBlocks, + "number of new blocks to simulate from the initial block height", + ) + flag.IntVar(&FlagBlockSizeValue, "BlockSize", defaultBlockSize, "operations per block") + flag.BoolVar(&FlagLeanValue, "Lean", false, "lean simulation log output") + flag.BoolVar(&FlagCommitValue, "Commit", false, "have the simulation commit") + flag.BoolVar(&FlagOnOperationValue, "SimulateEveryOperation", false, "run slow invariants every operation") + flag.BoolVar( + &FlagAllInvariantsValue, + "PrintAllInvariants", + false, + "print all invariants if a broken invariant is found", + ) + + // simulation flags + flag.BoolVar(&FlagEnabledValue, "Enabled", false, "enable the simulation") + flag.BoolVar(&FlagVerboseValue, "Verbose", false, "verbose log output") + flag.UintVar(&FlagPeriodValue, "Period", 0, "run slow invariants only once every period assertions") + flag.Int64Var( + &FlagGenesisTimeValue, + "GenesisTime", + 0, + "override genesis UNIX time instead of using a random UNIX time", + ) +} + +// NewConfigFromFlags creates a simulation from the retrieved values of the flags. +func NewConfigFromFlags() simulation.Config { + return simulation.Config{ + GenesisFile: FlagGenesisFileValue, + ParamsFile: FlagParamsFileValue, + ExportParamsPath: FlagExportParamsPathValue, + ExportParamsHeight: FlagExportParamsHeightValue, + ExportStatePath: FlagExportStatePathValue, + ExportStatsPath: FlagExportStatsPathValue, + Seed: FlagSeedValue, + InitialBlockHeight: FlagInitialBlockHeightValue, + NumBlocks: FlagNumBlocksValue, + BlockSize: FlagBlockSizeValue, + Lean: FlagLeanValue, + Commit: FlagCommitValue, + OnOperation: FlagOnOperationValue, + AllInvariants: FlagAllInvariantsValue, + } +} diff --git a/tests/simulation/sim/sim_state.go b/tests/simulation/sim/sim_state.go new file mode 100644 index 0000000000..2f314b6c78 --- /dev/null +++ b/tests/simulation/sim/sim_state.go @@ -0,0 +1,297 @@ +package sim + +import ( + "encoding/json" + "fmt" + "io" + "math/rand" + "os" + "time" + + "cosmossdk.io/math" + cmtjson "github.com/cometbft/cometbft/libs/json" + tmtypes "github.com/cometbft/cometbft/types" + "github.com/cosmos/cosmos-sdk/codec" + "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/types/module" + simtypes "github.com/cosmos/cosmos-sdk/types/simulation" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" + stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" + evmtypes "github.com/zeta-chain/ethermint/x/evm/types" + + zetaapp "github.com/zeta-chain/node/app" +) + +// Simulation parameter constants +const ( + StakePerAccount = "stake_per_account" + InitiallyBondedValidators = "initially_bonded_validators" +) + +// AppStateFn returns the initial application state using a genesis or the simulation parameters. +// It panics if the user provides files for both of them. +// If a file is not given for the genesis or the sim params, it creates a randomized one. +func AppStateFn( + cdc codec.Codec, + simManager *module.SimulationManager, + genesisState map[string]json.RawMessage, +) simtypes.AppStateFn { + return func(r *rand.Rand, accs []simtypes.Account, config simtypes.Config, + ) (appState json.RawMessage, simAccs []simtypes.Account, chainID string, genesisTimestamp time.Time) { + if FlagGenesisTimeValue == 0 { + genesisTimestamp = simtypes.RandTimestamp(r) + } else { + genesisTimestamp = time.Unix(FlagGenesisTimeValue, 0) + } + + chainID = config.ChainID + switch { + case config.ParamsFile != "" && config.GenesisFile != "": + panic("cannot provide both a genesis file and a params file") + + case config.GenesisFile != "": + // override the default chain-id from simapp to set it later to the config + genesisDoc, accounts, err := AppStateFromGenesisFileFn(r, cdc, config.GenesisFile) + if err != nil { + panic(err) + } + + if FlagGenesisTimeValue == 0 { + // use genesis timestamp if no custom timestamp is provided (i.e no random timestamp) + genesisTimestamp = genesisDoc.GenesisTime + } + + appState = genesisDoc.AppState + chainID = genesisDoc.ChainID + simAccs = accounts + + case config.ParamsFile != "": + appParams := make(simtypes.AppParams) + bz, err := os.ReadFile(config.ParamsFile) + if err != nil { + panic(err) + } + + err = json.Unmarshal(bz, &appParams) + if err != nil { + panic(err) + } + appState, simAccs = AppStateRandomizedFn( + simManager, + r, + cdc, + accs, + genesisTimestamp, + appParams, + genesisState, + ) + + default: + appParams := make(simtypes.AppParams) + appState, simAccs = AppStateRandomizedFn( + simManager, + r, + cdc, + accs, + genesisTimestamp, + appParams, + genesisState, + ) + } + + rawState := make(map[string]json.RawMessage) + err := json.Unmarshal(appState, &rawState) + if err != nil { + panic(err) + } + + stakingStateBz, ok := rawState[stakingtypes.ModuleName] + if !ok { + panic("staking genesis state is missing") + } + + stakingState := new(stakingtypes.GenesisState) + err = cdc.UnmarshalJSON(stakingStateBz, stakingState) + if err != nil { + panic(err) + } + + // compute not bonded balance + notBondedTokens := math.ZeroInt() + for _, val := range stakingState.Validators { + if val.Status != stakingtypes.Unbonded { + continue + } + notBondedTokens = notBondedTokens.Add(val.GetTokens()) + } + notBondedCoins := sdk.NewCoin(stakingState.Params.BondDenom, notBondedTokens) + + // edit bank state to make it have the not bonded pool tokens + bankStateBz, ok := rawState[banktypes.ModuleName] + if !ok { + panic("bank genesis state is missing") + } + bankState := new(banktypes.GenesisState) + err = cdc.UnmarshalJSON(bankStateBz, bankState) + if err != nil { + panic(err) + } + + stakingAddr := authtypes.NewModuleAddress(stakingtypes.NotBondedPoolName).String() + var found bool + for _, balance := range bankState.Balances { + if balance.Address == stakingAddr { + found = true + break + } + } + if !found { + bankState.Balances = append(bankState.Balances, banktypes.Balance{ + Address: stakingAddr, + Coins: sdk.NewCoins(notBondedCoins), + }) + } + + // Set the bond denom in the EVM genesis state + evmStateBz, ok := rawState[evmtypes.ModuleName] + if !ok { + panic("evm genesis state is missing") + } + + evmState := new(evmtypes.GenesisState) + cdc.MustUnmarshalJSON(evmStateBz, evmState) + + // we should replace the EvmDenom with BondDenom + evmState.Params.EvmDenom = stakingState.Params.BondDenom + + // change appState back + rawState[evmtypes.ModuleName] = cdc.MustMarshalJSON(evmState) + rawState[stakingtypes.ModuleName] = cdc.MustMarshalJSON(stakingState) + rawState[banktypes.ModuleName] = cdc.MustMarshalJSON(bankState) + + // replace appstate + appState, err = json.Marshal(rawState) + if err != nil { + panic(err) + } + return appState, simAccs, chainID, genesisTimestamp + } +} + +// AppStateRandomizedFn creates calls each module's GenesisState generator function +// and creates the simulation params +func AppStateRandomizedFn( + simManager *module.SimulationManager, r *rand.Rand, cdc codec.Codec, + accs []simtypes.Account, genesisTimestamp time.Time, appParams simtypes.AppParams, + genesisState map[string]json.RawMessage, +) (json.RawMessage, []simtypes.Account) { + numAccs := int64(len(accs)) + // generate a random amount of initial stake coins and a random initial + // number of bonded accounts + var ( + numInitiallyBonded int64 + initialStake math.Int + ) + + appParams.GetOrGenerate(cdc, + StakePerAccount, &initialStake, r, + func(r *rand.Rand) { initialStake = math.NewInt(r.Int63n(1e12)) }, + ) + appParams.GetOrGenerate(cdc, + InitiallyBondedValidators, &numInitiallyBonded, r, + func(r *rand.Rand) { numInitiallyBonded = int64(r.Intn(300)) }, + ) + + if numInitiallyBonded > numAccs { + numInitiallyBonded = numAccs + } + + // Set the default power reduction to be one less than the initial stake so that all randomised validators are part of the validator set + sdk.DefaultPowerReduction = initialStake.Sub(sdk.OneInt()) + + fmt.Printf( + `Selected randomly generated parameters for simulated genesis: +{ + stake_per_account: "%d", + initially_bonded_validators: "%d" +} +`, initialStake, numInitiallyBonded, + ) + + simState := &module.SimulationState{ + AppParams: appParams, + Cdc: cdc, + Rand: r, + GenState: genesisState, + Accounts: accs, + InitialStake: initialStake, + NumBonded: numInitiallyBonded, + GenTimestamp: genesisTimestamp, + } + + simManager.GenerateGenesisStates(simState) + + appState, err := json.Marshal(genesisState) + if err != nil { + panic(err) + } + + return appState, accs +} + +// AppStateFromGenesisFileFn util function to generate the genesis AppState +// from a genesis.json file. +func AppStateFromGenesisFileFn( + r io.Reader, + cdc codec.JSONCodec, + genesisFile string, +) (tmtypes.GenesisDoc, []simtypes.Account, error) { + bytes, err := os.ReadFile(genesisFile) + if err != nil { + panic(err) + } + + var genesis tmtypes.GenesisDoc + // NOTE: Comet uses a custom JSON decoder for GenesisDoc + err = cmtjson.Unmarshal(bytes, &genesis) + if err != nil { + panic(err) + } + + var appState zetaapp.GenesisState + err = json.Unmarshal(genesis.AppState, &appState) + if err != nil { + panic(err) + } + + var authGenesis authtypes.GenesisState + if appState[authtypes.ModuleName] != nil { + cdc.MustUnmarshalJSON(appState[authtypes.ModuleName], &authGenesis) + } + + newAccs := make([]simtypes.Account, len(authGenesis.Accounts)) + for i, acc := range authGenesis.Accounts { + // Pick a random private key, since we don't know the actual key + // This should be fine as it's only used for mock Tendermint validators + // and these keys are never actually used to sign by mock Tendermint. + privkeySeed := make([]byte, 15) + if _, err := r.Read(privkeySeed); err != nil { + panic(err) + } + + privKey := secp256k1.GenPrivKeyFromSecret(privkeySeed) + + a, ok := acc.GetCachedValue().(authtypes.AccountI) + if !ok { + return genesis, nil, fmt.Errorf("expected account") + } + + // create simulator accounts + simAcc := simtypes.Account{PrivKey: privKey, PubKey: privKey.PubKey(), Address: a.GetAddress()} + newAccs[i] = simAcc + } + + return genesis, newAccs, nil +} diff --git a/tests/simulation/sim/sim_utils.go b/tests/simulation/sim/sim_utils.go new file mode 100644 index 0000000000..b310d7cae2 --- /dev/null +++ b/tests/simulation/sim/sim_utils.go @@ -0,0 +1,68 @@ +package sim + +import ( + "fmt" + + dbm "github.com/cometbft/cometbft-db" + "github.com/cometbft/cometbft/libs/log" + "github.com/cosmos/cosmos-sdk/baseapp" + servertypes "github.com/cosmos/cosmos-sdk/server/types" + "github.com/zeta-chain/ethermint/app" + evmante "github.com/zeta-chain/ethermint/app/ante" + + zetaapp "github.com/zeta-chain/node/app" + "github.com/zeta-chain/node/app/ante" +) + +func NewSimApp( + logger log.Logger, + db dbm.DB, + appOptions servertypes.AppOptions, + baseAppOptions ...func(*baseapp.BaseApp), +) (*zetaapp.App, error) { + encCdc := zetaapp.MakeEncodingConfig() + + // Set load latest version to false as we manually set it later. + zetaApp := zetaapp.New( + logger, + db, + nil, + false, + map[int64]bool{}, + app.DefaultNodeHome, + 5, + encCdc, + appOptions, + baseAppOptions..., + ) + + // use zeta antehandler + options := ante.HandlerOptions{ + AccountKeeper: zetaApp.AccountKeeper, + BankKeeper: zetaApp.BankKeeper, + EvmKeeper: zetaApp.EvmKeeper, + FeeMarketKeeper: zetaApp.FeeMarketKeeper, + SignModeHandler: encCdc.TxConfig.SignModeHandler(), + SigGasConsumer: evmante.DefaultSigVerificationGasConsumer, + MaxTxGasWanted: 0, + ObserverKeeper: zetaApp.ObserverKeeper, + } + + anteHandler, err := ante.NewAnteHandler(options) + if err != nil { + panic(err) + } + + zetaApp.SetAnteHandler(anteHandler) + if err := zetaApp.LoadLatestVersion(); err != nil { + return nil, err + } + return zetaApp, nil +} + +// PrintStats prints the corresponding statistics from the app DB. +func PrintStats(db dbm.DB) { + fmt.Println("\nDB Stats") + fmt.Println(db.Stats()["leveldb.stats"]) + fmt.Println("GoLevelDB cached block size", db.Stats()["leveldb.cachedblock"]) +} diff --git a/tests/simulation/sim_test.go b/tests/simulation/sim_test.go new file mode 100644 index 0000000000..957e92081a --- /dev/null +++ b/tests/simulation/sim_test.go @@ -0,0 +1,213 @@ +package simulation_test + +import ( + "encoding/json" + "math/rand" + "os" + "testing" + + "github.com/stretchr/testify/require" + simutils "github.com/zeta-chain/node/tests/simulation/sim" + + "github.com/cosmos/cosmos-sdk/store" + + "github.com/cosmos/cosmos-sdk/baseapp" + "github.com/cosmos/cosmos-sdk/client/flags" + "github.com/cosmos/cosmos-sdk/server" + cosmossimutils "github.com/cosmos/cosmos-sdk/testutil/sims" + cosmossim "github.com/cosmos/cosmos-sdk/types/simulation" + "github.com/cosmos/cosmos-sdk/x/simulation" + cosmossimcli "github.com/cosmos/cosmos-sdk/x/simulation/client/cli" +) + +// AppChainID hardcoded chainID for simulation + +func init() { + simutils.GetSimulatorFlags() +} + +const ( + SimAppChainID = "simulation_777-1" + SimBlockMaxGas = 815000000 + //github.com/zeta-chain/node/issues/3004 + // TODO : Support pebbleDB for simulation tests + SimDBBackend = "goleveldb" + SimDBName = "simulation" +) + +// interBlockCacheOpt returns a BaseApp option function that sets the persistent +// inter-block write-through cache. +func interBlockCacheOpt() func(*baseapp.BaseApp) { + return baseapp.SetInterBlockCache(store.NewCommitKVStoreCacheManager()) +} + +// TestAppStateDeterminism runs a full application simulation , and produces multiple blocks as per the config +// It checks the determinism of the application by comparing the apphash at the end of each run to other runs +// The following test certifies that , for the same set of operations ( irrespective of what the operations are ) , +// we would reach the same final state if the initial state is the same +func TestAppStateDeterminism(t *testing.T) { + if !simutils.FlagEnabledValue { + t.Skip("skipping application simulation") + } + + config := simutils.NewConfigFromFlags() + + config.InitialBlockHeight = 1 + config.ExportParamsPath = "" + config.OnOperation = false + config.AllInvariants = false + config.ChainID = SimAppChainID + config.DBBackend = SimDBBackend + config.BlockMaxGas = SimBlockMaxGas + + numSeeds := 3 + numTimesToRunPerSeed := 5 + + // We will be overriding the random seed and just run a single simulation on the provided seed value + if config.Seed != cosmossimcli.DefaultSeedValue { + numSeeds = 1 + } + + appHashList := make([]json.RawMessage, numTimesToRunPerSeed) + + appOptions := make(cosmossimutils.AppOptionsMap, 0) + appOptions[server.FlagInvCheckPeriod] = simutils.FlagPeriodValue + + t.Log("Running tests for numSeeds: ", numSeeds, " numTimesToRunPerSeed: ", numTimesToRunPerSeed) + + for i := 0; i < numSeeds; i++ { + if config.Seed == cosmossimcli.DefaultSeedValue { + config.Seed = rand.Int63() + } + // For the same seed, the app hash produced at the end of each run should be the same + for j := 0; j < numTimesToRunPerSeed; j++ { + db, dir, logger, _, err := cosmossimutils.SetupSimulation( + config, + SimDBBackend, + SimDBName, + simutils.FlagVerboseValue, + simutils.FlagEnabledValue, + ) + require.NoError(t, err) + appOptions[flags.FlagHome] = dir + + simApp, err := simutils.NewSimApp( + logger, + db, + appOptions, + interBlockCacheOpt(), + baseapp.SetChainID(SimAppChainID), + ) + + t.Logf( + "running non-determinism simulation; seed %d: %d/%d, attempt: %d/%d\n", + config.Seed, i+1, numSeeds, j+1, numTimesToRunPerSeed, + ) + + blockedAddresses := simApp.ModuleAccountAddrs() + + // Random seed is used to produce a random initial state for the simulation + _, _, err = simulation.SimulateFromSeed( + t, + os.Stdout, + simApp.BaseApp, + simutils.AppStateFn( + simApp.AppCodec(), + simApp.SimulationManager(), + simApp.BasicManager().DefaultGenesis(simApp.AppCodec()), + ), + cosmossim.RandomAccounts, + cosmossimutils.SimulationOperations(simApp, simApp.AppCodec(), config), + blockedAddresses, + config, + simApp.AppCodec(), + ) + require.NoError(t, err) + + simutils.PrintStats(db) + + appHash := simApp.LastCommitID().Hash + appHashList[j] = appHash + + // Clean up resources + require.NoError(t, db.Close()) + require.NoError(t, os.RemoveAll(dir)) + + if j != 0 { + require.Equal( + t, + string(appHashList[0]), + string(appHashList[j]), + "non-determinism in seed %d: %d/%d, attempt: %d/%d\n", + config.Seed, + i+1, + numSeeds, + j+1, + numTimesToRunPerSeed, + ) + } + } + } +} + +// TestFullAppSimulation runs a full app simulation with the provided configuration. +// At the end of the run it tries to export the genesis state to make sure the export works. +func TestFullAppSimulation(t *testing.T) { + + config := simutils.NewConfigFromFlags() + + config.ChainID = SimAppChainID + config.BlockMaxGas = SimBlockMaxGas + config.DBBackend = SimDBBackend + + db, dir, logger, skip, err := cosmossimutils.SetupSimulation( + config, + SimDBBackend, + SimDBName, + simutils.FlagVerboseValue, + simutils.FlagEnabledValue, + ) + if skip { + t.Skip("skipping application simulation") + } + require.NoError(t, err, "simulation setup failed") + + defer func() { + require.NoError(t, db.Close()) + require.NoError(t, os.RemoveAll(dir)) + }() + appOptions := make(cosmossimutils.AppOptionsMap, 0) + appOptions[server.FlagInvCheckPeriod] = simutils.FlagPeriodValue + appOptions[flags.FlagHome] = dir + + simApp, err := simutils.NewSimApp(logger, db, appOptions, interBlockCacheOpt(), baseapp.SetChainID(SimAppChainID)) + require.NoError(t, err) + + blockedAddresses := simApp.ModuleAccountAddrs() + _, _, simerr := simulation.SimulateFromSeed( + t, + os.Stdout, + simApp.BaseApp, + simutils.AppStateFn( + simApp.AppCodec(), + simApp.SimulationManager(), + simApp.BasicManager().DefaultGenesis(simApp.AppCodec()), + ), + cosmossim.RandomAccounts, + cosmossimutils.SimulationOperations(simApp, simApp.AppCodec(), config), + blockedAddresses, + config, + simApp.AppCodec(), + ) + require.NoError(t, simerr) + + // check export works as expected + exported, err := simApp.ExportAppStateAndValidators(false, nil, nil) + require.NoError(t, err) + if config.ExportStatePath != "" { + err := os.WriteFile(config.ExportStatePath, exported.AppState, 0o600) + require.NoError(t, err) + } + + simutils.PrintStats(db) +} diff --git a/testutil/simapp/default_constants.go b/testutil/simapp/default_constants.go deleted file mode 100644 index c4398ad7bb..0000000000 --- a/testutil/simapp/default_constants.go +++ /dev/null @@ -1,25 +0,0 @@ -package simapp - -import ( - "time" - - tmproto "github.com/cometbft/cometbft/proto/tendermint/types" - tmtypes "github.com/cometbft/cometbft/types" -) - -var defaultConsensusParams = &tmproto.ConsensusParams{ - Block: &tmproto.BlockParams{ - MaxBytes: 200000, - MaxGas: 2000000, - }, - Evidence: &tmproto.EvidenceParams{ - MaxAgeNumBlocks: 302400, - MaxAgeDuration: 504 * time.Hour, // 3 weeks is the max duration - MaxBytes: 10000, - }, - Validator: &tmproto.ValidatorParams{ - PubKeyTypes: []string{ - tmtypes.ABCIPubKeyTypeEd25519, - }, - }, -} diff --git a/testutil/simapp/simapp.go b/testutil/simapp/simapp.go deleted file mode 100644 index b42b0b5f1a..0000000000 --- a/testutil/simapp/simapp.go +++ /dev/null @@ -1,219 +0,0 @@ -package simapp - -import ( - "encoding/json" - "testing" - "time" - - tmdb "github.com/cometbft/cometbft-db" - abci "github.com/cometbft/cometbft/abci/types" - "github.com/cometbft/cometbft/libs/log" - tmproto "github.com/cometbft/cometbft/proto/tendermint/types" - tmtypes "github.com/cometbft/cometbft/types" - codectypes "github.com/cosmos/cosmos-sdk/codec/types" - cryptocodec "github.com/cosmos/cosmos-sdk/crypto/codec" - simtestutil "github.com/cosmos/cosmos-sdk/testutil/sims" - sdk "github.com/cosmos/cosmos-sdk/types" - authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" - banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" - stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" - "github.com/stretchr/testify/require" - - "github.com/zeta-chain/node/app" - "github.com/zeta-chain/node/cmd/zetacored/config" - types2 "github.com/zeta-chain/node/x/emissions/types" -) - -func Setup(isCheckTx bool) *app.App { - app, genesisState := setup(!isCheckTx, 5) - if !isCheckTx { - // init chain must be called to stop deliverState from being nil - stateBytes, err := json.MarshalIndent(genesisState, "", " ") - if err != nil { - panic(err) - } - - // Initialize the chain - app.InitChain( - abci.RequestInitChain{ - ChainId: "simnet_101-1", - Validators: []abci.ValidatorUpdate{}, - ConsensusParams: defaultConsensusParams, - AppStateBytes: stateBytes, - }, - ) - } - - return app -} - -func setup(withGenesis bool, invCheckPeriod uint) (*app.App, app.GenesisState) { - db := tmdb.NewMemDB() - encCdc := app.MakeEncodingConfig() - a := app.New( - log.NewNopLogger(), - db, - nil, - true, - map[int64]bool{}, - app.DefaultNodeHome, - invCheckPeriod, - encCdc, - simtestutil.EmptyAppOptions{}, - ) - if withGenesis { - return a, app.NewDefaultGenesisState(encCdc.Codec) - } - return a, app.GenesisState{} -} - -func SetupWithGenesisValSet( - t *testing.T, - valSet *tmtypes.ValidatorSet, - genDelAccs []authtypes.GenesisAccount, - bondAmt sdk.Int, - emissionParams types2.Params, - genDelBalances []banktypes.Balance, - genBalances []banktypes.Balance, -) *app.App { - app, genesisState := setup(true, 5) - // set genesis accounts - authGenesis := authtypes.NewGenesisState(authtypes.DefaultParams(), genDelAccs) - genesisState[authtypes.ModuleName] = app.AppCodec().MustMarshalJSON(authGenesis) - - validators := make([]stakingtypes.Validator, 0, len(valSet.Validators)) - delegations := make([]stakingtypes.Delegation, 0, len(valSet.Validators)) - // Make all members of valSet as validators - // Make all members of delSet as delegators to each of the validators - for _, val := range valSet.Validators { - pk, err := cryptocodec.FromTmPubKeyInterface(val.PubKey) - require.NoError(t, err) - pkAny, err := codectypes.NewAnyWithValue(pk) - require.NoError(t, err) - validator := stakingtypes.Validator{ - OperatorAddress: sdk.ValAddress(val.Address).String(), - ConsensusPubkey: pkAny, - Jailed: false, - Status: stakingtypes.Bonded, - Tokens: bondAmt, - DelegatorShares: sdk.OneDec(), - Description: stakingtypes.Description{}, - UnbondingHeight: int64(0), - UnbondingTime: time.Unix(0, 0).UTC(), - Commission: stakingtypes.NewCommission(sdk.ZeroDec(), sdk.ZeroDec(), sdk.ZeroDec()), - MinSelfDelegation: sdk.ZeroInt(), - } - validators = append(validators, validator) - delegations = append( - delegations, - stakingtypes.NewDelegation(genDelAccs[0].GetAddress(), val.Address.Bytes(), sdk.OneDec()), - ) - } - - emissionsGenesis := types2.DefaultGenesis() - emissionsGenesis.Params = emissionParams - genesisState[types2.ModuleName] = app.AppCodec().MustMarshalJSON(emissionsGenesis) - // set validators and delegations - params := stakingtypes.DefaultParams() - params.BondDenom = config.BaseDenom - stakingGenesis := stakingtypes.NewGenesisState(params, validators, delegations) - genesisState[stakingtypes.ModuleName] = app.AppCodec().MustMarshalJSON(stakingGenesis) - - totalSupply := sdk.NewCoins() - // genDelBalances contains additional balances for delegators - // Add Bond amount and additional coins for these addresses - - for _, b := range genDelBalances { - // add genesis acc tokens and delegated tokens to total supply - totalSupply = totalSupply.Add(b.Coins.Add(sdk.NewCoin(config.BaseDenom, bondAmt))...) - } - // add balances for non delegator accounts - // Add only external balances - for _, b := range genBalances { - // add genesis acc tokens and delegated tokens to total supply - totalSupply = totalSupply.Add(b.Coins...) - } - - totalBalances := []banktypes.Balance{} - // Add extra balance to account for delegator bonded pool - totalBalances = append(append(append(totalBalances, genBalances...), genDelBalances...), banktypes.Balance{ - Address: authtypes.NewModuleAddress(stakingtypes.BondedPoolName).String(), - Coins: sdk.Coins{sdk.NewCoin(config.BaseDenom, bondAmt)}, - }) - - // update total supply - - bankGenesis := banktypes.NewGenesisState( - banktypes.DefaultGenesisState().Params, - totalBalances, - totalSupply, - []banktypes.Metadata{}, - []banktypes.SendEnabled{}, - ) - genesisState[banktypes.ModuleName] = app.AppCodec().MustMarshalJSON(bankGenesis) - - stateBytes, err := json.MarshalIndent(genesisState, "", " ") - require.NoError(t, err) - - // init chain will set the validator set and initialize the genesis accounts - app.InitChain( - abci.RequestInitChain{ - ChainId: "simnet_101-1", - Validators: []abci.ValidatorUpdate{}, - ConsensusParams: defaultConsensusParams, - AppStateBytes: stateBytes, - }, - ) - - // commit genesis changes - app.Commit() - - app.BeginBlock(abci.RequestBeginBlock{Header: tmproto.Header{ - Height: app.LastBlockHeight() + 1, - AppHash: app.LastCommitID().Hash, - ValidatorsHash: valSet.Hash(), - NextValidatorsHash: valSet.Hash(), - ChainID: "simnet_101-1", - }}) - - return app -} - -func SetupWithGenesisAccounts(genAccs []authtypes.GenesisAccount, balances ...banktypes.Balance) *app.App { - app, genesisState := setup(true, 0) - authGenesis := authtypes.NewGenesisState(authtypes.DefaultParams(), genAccs) - genesisState[authtypes.ModuleName] = app.AppCodec().MustMarshalJSON(authGenesis) - - totalSupply := sdk.NewCoins() - for _, b := range balances { - totalSupply = totalSupply.Add(b.Coins...) - } - - bankGenesis := banktypes.NewGenesisState( - banktypes.DefaultGenesisState().Params, - balances, - totalSupply, - []banktypes.Metadata{}, - []banktypes.SendEnabled{}, - ) - genesisState[banktypes.ModuleName] = app.AppCodec().MustMarshalJSON(bankGenesis) - - stateBytes, err := json.MarshalIndent(genesisState, "", " ") - if err != nil { - panic(err) - } - - app.InitChain( - abci.RequestInitChain{ - Validators: []abci.ValidatorUpdate{}, - ConsensusParams: defaultConsensusParams, - AppStateBytes: stateBytes, - }, - ) - - app.Commit() - - app.BeginBlock(abci.RequestBeginBlock{Header: tmproto.Header{Height: app.LastBlockHeight() + 1}}) - - return app -}