From 7c5e0afb091392f159d7a10571cdc0badd0ee136 Mon Sep 17 00:00:00 2001 From: Federico Kunze <31522760+fedekunze@users.noreply.github.com> Date: Wed, 29 Apr 2020 11:48:44 -0400 Subject: [PATCH] SDK modules simulations (#269) * SDK modules simulations * changelog * changelog --- CHANGELOG.md | 12 +- Makefile | 56 +++++++- app/simulation_test.go | 295 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 355 insertions(+), 8 deletions(-) create mode 100644 app/simulation_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 61f3ec11..e212066c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -61,12 +61,12 @@ Ref: https://keepachangelog.com/en/1.0.0/ * Update uninstallFilter and getFilterChanges accordingly * uninstallFilter stops the polling goroutine * getFilterChanges returns the filter's internal list of block hashes and resets it - -* (rpc) [\#54](https://github.com/ChainSafe/ethermint/issues/54) [\#55](https://github.com/ChainSafe/ethermint/issues/55) - Implement eth_getFilterLogs and eth_getLogs - * for a given filter, look through each block for transactions. If there are transactions in the block, get the logs from it, and filter using the filterLogs method - * eth_getLogs and eth_getFilterChanges for log filters use the same underlying method as eth_getFilterLogs - * update HandleMsgEthereumTx to store logs using the ethereum hash +* (rpc) [\#54](https://github.com/ChainSafe/ethermint/issues/54), [\#55](https://github.com/ChainSafe/ethermint/issues/55) + Implement `eth_getFilterLogs` and `eth_getLogs`: + * For a given filter, look through each block for transactions. If there are transactions in the block, get the logs from it, and filter using the filterLogs method + * `eth_getLogs` and `eth_getFilterChanges` for log filters use the same underlying method as `eth_getFilterLogs` + * update `HandleMsgEthereumTx` to store logs using the ethereum hash +* (app) [\#187](https://github.com/ChainSafe/ethermint/issues/187) Add support for simulations. ### Bug Fixes diff --git a/Makefile b/Makefile index 99e3e8eb..9426634a 100644 --- a/Makefile +++ b/Makefile @@ -20,6 +20,9 @@ DOCKER_IMAGE = cosmos/ethermint ETHERMINT_DAEMON_BINARY = emintd ETHERMINT_CLI_BINARY = emintcli GO_MOD=GO111MODULE=on +BINDIR ?= $(GOPATH)/bin +SIMAPP = github.com/cosmos/ethermint/app +RUNSIM = $(BINDIR)/runsim all: tools verify install @@ -77,7 +80,17 @@ MISSPELL_CHECK := $(shell command -v misspell 2> /dev/null) ERRCHECK_CHECK := $(shell command -v errcheck 2> /dev/null) UNPARAM_CHECK := $(shell command -v unparam 2> /dev/null) -tools: + +# Install the runsim binary with a temporary workaround of entering an outside +# directory as the "go get" command ignores the -mod option and will polute the +# go.{mod, sum} files. +# +# ref: https://github.com/golang/go/issues/30515 +$(RUNSIM): + @echo "Installing runsim..." + @(cd /tmp && go get github.com/cosmos/tools/cmd/runsim@v1.0.0) + +tools: $(RUNSIM) ifdef GOLINT_CHECK @echo "Golint is already installed. Run 'make update-tools' to update." else @@ -238,4 +251,43 @@ proto-update-deps: @sed -i '' '6 s|x/auth/types/types.proto|third_party/proto/cosmos-sdk/x/auth/types/types.proto|g' $(SUPPLY_PROTO_TYPES)/types.proto -.PHONY: proto-all proto-gen proto-lint proto-check-breaking proto-update-deps \ No newline at end of file +.PHONY: proto-all proto-gen proto-lint proto-check-breaking proto-update-deps + +####################### +### Simulations ### +####################### + +test-sim-nondeterminism: + @echo "Running non-determinism test..." + @go test -mod=readonly $(SIMAPP) -run TestAppStateDeterminism -Enabled=true \ + -NumBlocks=100 -BlockSize=200 -Commit=true -Period=0 -v -timeout 24h + +test-sim-custom-genesis-fast: + @echo "Running custom genesis simulation..." + @echo "By default, ${HOME}/.emintd/config/genesis.json will be used." + @go test -mod=readonly $(SIMAPP) -run TestFullAppSimulation -Genesis=${HOME}/.emintd/config/genesis.json \ + -Enabled=true -NumBlocks=100 -BlockSize=200 -Commit=true -Seed=99 -Period=5 -v -timeout 24h + +test-sim-import-export: runsim + @echo "Running Ethermint import/export simulation. This may take several minutes..." + @$(BINDIR)/runsim -Jobs=4 -SimAppPkg=$(SIMAPP) 25 5 TestAppImportExport + +test-sim-after-import: runsim + @echo "Running Ethermint simulation-after-import. This may take several minutes..." + @$(BINDIR)/runsim -Jobs=4 -SimAppPkg=$(SIMAPP) 25 5 TestAppSimulationAfterImport + +test-sim-custom-genesis-multi-seed: runsim + @echo "Running multi-seed custom genesis simulation..." + @echo "By default, ${HOME}/.emintd/config/genesis.json will be used." + @$(BINDIR)/runsim -Jobs=4 -Genesis=${HOME}/.emintd/config/genesis.json 400 5 TestFullAppSimulation + +test-sim-multi-seed-long: runsim + @echo "Running multi-seed application simulation. This may take awhile!" + @$(BINDIR)/runsim -Jobs=4 -SimAppPkg=$(SIMAPP) 500 50 TestFullAppSimulation + +test-sim-multi-seed-short: runsim + @echo "Running multi-seed application simulation. This may take awhile!" + @$(BINDIR)/runsim -Jobs=4 -SimAppPkg=$(SIMAPP) 50 10 TestFullAppSimulation + +.PHONY: runsim test-sim-nondeterminism test-sim-custom-genesis-fast test-sim-fast sim-import-export \ + test-sim-simulation-after-import test-sim-custom-genesis-multi-seed test-sim-multi-seed \ \ No newline at end of file diff --git a/app/simulation_test.go b/app/simulation_test.go new file mode 100644 index 00000000..1e60f49e --- /dev/null +++ b/app/simulation_test.go @@ -0,0 +1,295 @@ +package app + +import ( + "encoding/json" + "fmt" + "math/rand" + "os" + "testing" + + "github.com/stretchr/testify/require" + abci "github.com/tendermint/tendermint/abci/types" + "github.com/tendermint/tendermint/libs/log" + dbm "github.com/tendermint/tm-db" + + "github.com/cosmos/cosmos-sdk/baseapp" + "github.com/cosmos/cosmos-sdk/simapp" + "github.com/cosmos/cosmos-sdk/simapp/helpers" + "github.com/cosmos/cosmos-sdk/store" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/auth" + distr "github.com/cosmos/cosmos-sdk/x/distribution" + "github.com/cosmos/cosmos-sdk/x/gov" + "github.com/cosmos/cosmos-sdk/x/mint" + "github.com/cosmos/cosmos-sdk/x/params" + "github.com/cosmos/cosmos-sdk/x/simulation" + "github.com/cosmos/cosmos-sdk/x/slashing" + "github.com/cosmos/cosmos-sdk/x/staking" + "github.com/cosmos/cosmos-sdk/x/supply" +) + +func init() { + simapp.GetSimulatorFlags() +} + +type storeKeysPrefixes struct { + A sdk.StoreKey + B sdk.StoreKey + Prefixes [][]byte +} + +// fauxMerkleModeOpt returns a BaseApp option to use a dbStoreAdapter instead of +// an IAVLStore for faster simulation speed. +func fauxMerkleModeOpt(bapp *baseapp.BaseApp) { + bapp.SetFauxMerkleMode() +} + +// 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()) +} + +func TestFullAppSimulation(t *testing.T) { + config, db, dir, logger, skip, err := simapp.SetupSimulation("leveldb-app-sim", "Simulation") + if skip { + t.Skip("skipping application simulation") + } + require.NoError(t, err, "simulation setup failed") + + defer func() { + db.Close() + require.NoError(t, os.RemoveAll(dir)) + }() + + app := NewEthermintApp(logger, db, nil, true, simapp.FlagPeriodValue, fauxMerkleModeOpt) + require.Equal(t, appName, app.Name()) + + // run randomized simulation + _, simParams, simErr := simulation.SimulateFromSeed( + t, os.Stdout, app.BaseApp, simapp.AppStateFn(app.Codec(), app.SimulationManager()), + simapp.SimulationOperations(app, app.Codec(), config), + app.ModuleAccountAddrs(), config, + ) + + // export state and simParams before the simulation error is checked + err = simapp.CheckExportSimulation(app, config, simParams) + require.NoError(t, err) + require.NoError(t, simErr) + + if config.Commit { + simapp.PrintStats(db) + } +} + +func TestAppImportExport(t *testing.T) { + config, db, dir, logger, skip, err := simapp.SetupSimulation("leveldb-app-sim", "Simulation") + if skip { + t.Skip("skipping application import/export simulation") + } + require.NoError(t, err, "simulation setup failed") + + defer func() { + db.Close() + require.NoError(t, os.RemoveAll(dir)) + }() + + app := NewEthermintApp(logger, db, nil, true, simapp.FlagPeriodValue, fauxMerkleModeOpt) + require.Equal(t, appName, app.Name()) + + // Run randomized simulation + _, simParams, simErr := simulation.SimulateFromSeed( + t, os.Stdout, app.BaseApp, simapp.AppStateFn(app.Codec(), app.SimulationManager()), + simapp.SimulationOperations(app, app.Codec(), config), + app.ModuleAccountAddrs(), config, + ) + + // export state and simParams before the simulation error is checked + err = simapp.CheckExportSimulation(app, config, simParams) + require.NoError(t, err) + require.NoError(t, simErr) + + if config.Commit { + simapp.PrintStats(db) + } + + fmt.Printf("exporting genesis...\n") + + appState, _, err := app.ExportAppStateAndValidators(false, []string{}) + require.NoError(t, err) + + fmt.Printf("importing genesis...\n") + + // nolint: dogsled + _, newDB, newDir, _, _, err := simapp.SetupSimulation("leveldb-app-sim-2", "Simulation-2") + require.NoError(t, err, "simulation setup failed") + + defer func() { + newDB.Close() + require.NoError(t, os.RemoveAll(newDir)) + }() + + newApp := NewEthermintApp(log.NewNopLogger(), newDB, nil, true, simapp.FlagPeriodValue, fauxMerkleModeOpt) + require.Equal(t, appName, newApp.Name()) + + var genesisState map[string]json.RawMessage + err = app.Codec().UnmarshalJSON(appState, &genesisState) + require.NoError(t, err) + + ctxA := app.NewContext(true, abci.Header{Height: app.LastBlockHeight()}) + ctxB := newApp.NewContext(true, abci.Header{Height: app.LastBlockHeight()}) + newApp.mm.InitGenesis(ctxB, app.Codec(), genesisState) + + fmt.Printf("comparing stores...\n") + + storeKeysPrefixes := []storeKeysPrefixes{ + {app.keys[baseapp.MainStoreKey], newApp.keys[baseapp.MainStoreKey], [][]byte{}}, + {app.keys[auth.StoreKey], newApp.keys[auth.StoreKey], [][]byte{}}, + {app.keys[staking.StoreKey], newApp.keys[staking.StoreKey], + [][]byte{ + staking.UnbondingQueueKey, staking.RedelegationQueueKey, staking.ValidatorQueueKey, + staking.HistoricalInfoKey, + }}, // ordering may change but it doesn't matter + {app.keys[slashing.StoreKey], newApp.keys[slashing.StoreKey], [][]byte{}}, + {app.keys[mint.StoreKey], newApp.keys[mint.StoreKey], [][]byte{}}, + {app.keys[distr.StoreKey], newApp.keys[distr.StoreKey], [][]byte{}}, + {app.keys[supply.StoreKey], newApp.keys[supply.StoreKey], [][]byte{}}, + {app.keys[params.StoreKey], newApp.keys[params.StoreKey], [][]byte{}}, + {app.keys[gov.StoreKey], newApp.keys[gov.StoreKey], [][]byte{}}, + } + + for _, skp := range storeKeysPrefixes { + storeA := ctxA.KVStore(skp.A) + storeB := ctxB.KVStore(skp.B) + + failedKVAs, failedKVBs := sdk.DiffKVStores(storeA, storeB, skp.Prefixes) + require.Equal(t, len(failedKVAs), len(failedKVBs), "unequal sets of key-values to compare") + + fmt.Printf("compared %d key/value pairs between %s and %s\n", len(failedKVAs), skp.A, skp.B) + require.Equal(t, len(failedKVAs), 0, simapp.GetSimulationLog(skp.A.Name(), app.SimulationManager().StoreDecoders, app.Codec(), failedKVAs, failedKVBs)) + } +} + +func TestAppSimulationAfterImport(t *testing.T) { + config, db, dir, logger, skip, err := simapp.SetupSimulation("leveldb-app-sim", "Simulation") + if skip { + t.Skip("skipping application simulation after import") + } + require.NoError(t, err, "simulation setup failed") + + defer func() { + db.Close() + require.NoError(t, os.RemoveAll(dir)) + }() + + app := NewEthermintApp(logger, db, nil, true, simapp.FlagPeriodValue, fauxMerkleModeOpt) + require.Equal(t, appName, app.Name()) + + // Run randomized simulation + stopEarly, simParams, simErr := simulation.SimulateFromSeed( + t, os.Stdout, app.BaseApp, simapp.AppStateFn(app.Codec(), app.SimulationManager()), + simapp.SimulationOperations(app, app.Codec(), config), + app.ModuleAccountAddrs(), config, + ) + + // export state and simParams before the simulation error is checked + err = simapp.CheckExportSimulation(app, config, simParams) + require.NoError(t, err) + require.NoError(t, simErr) + + if config.Commit { + simapp.PrintStats(db) + } + + if stopEarly { + fmt.Println("can't export or import a zero-validator genesis, exiting test...") + return + } + + fmt.Printf("exporting genesis...\n") + + appState, _, err := app.ExportAppStateAndValidators(true, []string{}) + require.NoError(t, err) + + fmt.Printf("importing genesis...\n") + + // nolint: dosgsled + _, newDB, newDir, _, _, err := simapp.SetupSimulation("leveldb-app-sim-2", "Simulation-2") + require.NoError(t, err, "simulation setup failed") + + defer func() { + newDB.Close() + require.NoError(t, os.RemoveAll(newDir)) + }() + + newApp := NewEthermintApp(log.NewNopLogger(), newDB, nil, true, simapp.FlagPeriodValue, fauxMerkleModeOpt) + require.Equal(t, appName, newApp.Name()) + + newApp.InitChain(abci.RequestInitChain{ + AppStateBytes: appState, + }) + + _, _, err = simulation.SimulateFromSeed( + t, os.Stdout, newApp.BaseApp, simapp.AppStateFn(app.Codec(), app.SimulationManager()), + simapp.SimulationOperations(newApp, newApp.Codec(), config), + newApp.ModuleAccountAddrs(), config, + ) + require.NoError(t, err) +} + +func TestAppStateDeterminism(t *testing.T) { + if !simapp.FlagEnabledValue { + t.Skip("skipping application simulation") + } + + config := simapp.NewConfigFromFlags() + config.InitialBlockHeight = 1 + config.ExportParamsPath = "" + config.OnOperation = false + config.AllInvariants = false + config.ChainID = helpers.SimAppChainID + + numTimesToRunPerSeed := 2 + appHashList := make([]json.RawMessage, numTimesToRunPerSeed) + + config.Seed = rand.Int63() + + for i := 0; i < numTimesToRunPerSeed; i++ { + var logger log.Logger + if simapp.FlagVerboseValue { + logger = log.TestingLogger() + } else { + logger = log.NewNopLogger() + } + + db := dbm.NewMemDB() + + app := NewEthermintApp(logger, db, nil, true, simapp.FlagPeriodValue, interBlockCacheOpt()) + + fmt.Printf( + "running non-determinism simulation; seed %d: attempt: %d/%d\n", + config.Seed, i+1, numTimesToRunPerSeed, + ) + + _, _, err := simulation.SimulateFromSeed( + t, os.Stdout, app.BaseApp, simapp.AppStateFn(app.Codec(), app.SimulationManager()), + simapp.SimulationOperations(app, app.Codec(), config), + app.ModuleAccountAddrs(), config, + ) + require.NoError(t, err) + + if config.Commit { + simapp.PrintStats(db) + } + + appHash := app.LastCommitID().Hash + appHashList[i] = appHash + + if i != 0 { + require.Equal( + t, appHashList[0], appHashList[i], + "non-determinism in seed %d: %d/%d, attempt: %d/%d\n", config.Seed, i+1, numTimesToRunPerSeed, + ) + } + } +}