diff --git a/Gopkg.lock b/Gopkg.lock index 25c5dc489..6f34de9a4 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -68,6 +68,14 @@ pruneopts = "UT" revision = "d4cc87b860166d00d6b5b9e0d3b3d71d6088d4d4" +[[projects]] + digest = "1:3d43e35d7159c669fd70a9499170ad06a85bdb075f7db1e3b9cc604c17a9293b" + name = "github.com/cosmos/cosmos-sdk" + packages = ["codec"] + pruneopts = "UT" + revision = "dfd00a661a19df6efe7e0dde96b2fbeb883d1d80" + version = "v0.27.1" + [[projects]] digest = "1:e8a3550c8786316675ff54ad6f09d265d129c9d986919af7f541afba50d87ce2" name = "github.com/cosmos/go-bip39" @@ -867,6 +875,7 @@ "github.com/bartekn/go-bip39", "github.com/bgentry/speakeasy", "github.com/btcsuite/btcd/btcec", + "github.com/cosmos/cosmos-sdk/codec", "github.com/cosmos/go-bip39", "github.com/emicklei/proto", "github.com/go-kit/kit/metrics", diff --git a/Makefile b/Makefile index d9150f5a6..770626ee3 100644 --- a/Makefile +++ b/Makefile @@ -120,7 +120,7 @@ test_sim_modules: test_sim_benchmark: @echo "Running benchmark test..." - @go test ./app -run=none -bench=BenchmarkFullIrisSimulation -v -SimulationCommit=true -SimulationNumBlocks=100 -timeout 24h + @go test ./app -run=none -bench=BenchmarkFullIrisSimulation -v -SimulationCommit=true -SimulationNumBlocks=100 -SimulationCommit=true -timeout 24h test_sim_iris_nondeterminism: @echo "Running nondeterminism test..." @@ -128,11 +128,11 @@ test_sim_iris_nondeterminism: test_sim_iris_fast: @echo "Running quick Iris simulation. This may take several minutes..." - @go test ./app -run TestFullIrisSimulation -v -SimulationEnabled=true -SimulationNumBlocks=100 -timeout 24h + @go test ./app -run TestFullIrisSimulation -v -SimulationEnabled=true -SimulationNumBlocks=100 -SimulationBlockSize=200 -SimulationCommit=true -SimulationSeed=99 -timeout 24h test_sim_iris_slow: @echo "Running full Iris simulation. This may take awhile!" - @go test ./app -run TestFullIrisSimulation -v -SimulationEnabled=true -SimulationNumBlocks=1000 -SimulationVerbose=true -timeout 24h + @go test ./app -run TestFullIrisSimulation -v -SimulationEnabled=true -SimulationNumBlocks=1000 -SimulationBlockSize=200 -SimulationCommit=true -SimulationSeed=99 -timeout 24h testnet_init: @echo "Work well only when Bech32PrefixAccAddr equal faa" diff --git a/app/app.go b/app/app.go index 879223c9a..ca94c40f9 100644 --- a/app/app.go +++ b/app/app.go @@ -1,7 +1,6 @@ package app import ( - "encoding/json" "fmt" "io" "os" @@ -32,7 +31,6 @@ import ( cmn "github.com/tendermint/tendermint/libs/common" dbm "github.com/tendermint/tendermint/libs/db" "github.com/tendermint/tendermint/libs/log" - tmtypes "github.com/tendermint/tendermint/types" "time" "github.com/irisnet/irishub/modules/guardian" ) @@ -40,6 +38,7 @@ import ( const ( appName = "IrisApp" FlagReplay = "replay" + FlagReplayHeight = "replay_height" ) // default home directories for expected binaries @@ -247,6 +246,11 @@ func (app *IrisApp) mountStoreAndSetupBaseApp(lastHeight int64) { var err error if viper.GetBool(FlagReplay) { err = app.LoadVersion(lastHeight, app.keyMain, true) + } else if viper.GetInt64(FlagReplayHeight) > 0 { + replayHeight := viper.GetInt64(FlagReplayHeight) + loadHeight := bam.ReplayToHeight(replayHeight, app.Logger) + app.Logger.Info(fmt.Sprintf("Load store at %d, start to replay to %d", loadHeight, replayHeight)) + err = app.LoadVersion(loadHeight, app.keyMain, true) } else { err = app.LoadLatestVersion(app.keyMain) } @@ -320,6 +324,10 @@ func (app *IrisApp) EndBlocker(ctx sdk.Context, req abci.RequestEndBlock) abci.R validatorUpdates := stake.EndBlocker(ctx, app.stakeKeeper) tags = tags.AppendTags(upgrade.EndBlocker(ctx, app.upgradeKeeper)) tags = tags.AppendTags(service.EndBlocker(ctx, app.serviceKeeper)) + height := ctx.BlockHeight() + _=height + app.assertRuntimeInvariants() + return abci.ResponseEndBlock{ ValidatorUpdates: validatorUpdates, Tags: tags, @@ -413,53 +421,6 @@ func (app *IrisApp) initChainer(ctx sdk.Context, req abci.RequestInitChain) abci } } -// export the state of iris for a genesis file -func (app *IrisApp) ExportAppStateAndValidators() (appState json.RawMessage, validators []tmtypes.GenesisValidator, err error) { - ctx := app.NewContext(true, abci.Header{}) - - // iterate to get the accounts - accounts := []GenesisAccount{} - appendAccount := func(acc auth.Account) (stop bool) { - account := NewGenesisAccountI(acc) - accounts = append(accounts, account) - return false - } - app.accountMapper.IterateAccounts(ctx, appendAccount) - fileAccounts := []GenesisFileAccount{} - for _, acc := range accounts { - var coinsString []string - for _, coin := range acc.Coins { - coinsString = append(coinsString, coin.String()) - } - fileAccounts = append(fileAccounts, - GenesisFileAccount{ - Address: acc.Address, - Coins: coinsString, - Sequence: acc.Sequence, - AccountNumber: acc.AccountNumber, - }) - } - genState := NewGenesisFileState( - fileAccounts, - auth.ExportGenesis(ctx, app.feeCollectionKeeper), - stake.ExportGenesis(ctx, app.stakeKeeper), - mint.ExportGenesis(ctx, app.mintKeeper), - distr.ExportGenesis(ctx, app.distrKeeper), - gov.ExportGenesis(ctx, app.govKeeper), - upgrade.WriteGenesis(ctx), - service.ExportGenesis(ctx, app.serviceKeeper), - arbitration.ExportGenesis(ctx), - guardian.ExportGenesis(ctx, app.guardianKeeper), - slashing.ExportGenesis(ctx, app.slashingKeeper), - ) - appState, err = codec.MarshalJSONIndent(app.cdc, genState) - if err != nil { - return nil, nil, err - } - validators = stake.WriteValidators(ctx, app.stakeKeeper) - return appState, validators, nil -} - // Iterates through msgs and executes them func (app *IrisApp) runMsgs(ctx sdk.Context, msgs []sdk.Msg, mode bam.RunTxMode) (result sdk.Result) { // accumulate results diff --git a/app/export.go b/app/export.go new file mode 100644 index 000000000..294a2a2cc --- /dev/null +++ b/app/export.go @@ -0,0 +1,166 @@ +package app + +import ( + "encoding/json" + "fmt" + + "github.com/irisnet/irishub/codec" + sdk "github.com/irisnet/irishub/types" + "github.com/irisnet/irishub/modules/auth" + distr "github.com/irisnet/irishub/modules/distribution" + "github.com/irisnet/irishub/modules/gov" + "github.com/irisnet/irishub/modules/mint" + "github.com/irisnet/irishub/modules/slashing" + stake "github.com/irisnet/irishub/modules/stake" + abci "github.com/tendermint/tendermint/abci/types" + tmtypes "github.com/tendermint/tendermint/types" + "github.com/irisnet/irishub/modules/upgrade" + "github.com/irisnet/irishub/modules/service" + "github.com/irisnet/irishub/modules/arbitration" + "github.com/irisnet/irishub/modules/guardian" +) + +// export the state of gaia for a genesis file +func (app *IrisApp) ExportAppStateAndValidators(forZeroHeight bool) ( + appState json.RawMessage, validators []tmtypes.GenesisValidator, err error) { + + // as if they could withdraw from the start of the next block + ctx := app.NewContext(true, abci.Header{Height: app.LastBlockHeight()}) + + if forZeroHeight { + app.prepForZeroHeightGenesis(ctx) + } + + // iterate to get the accounts + accounts := []GenesisAccount{} + appendAccount := func(acc auth.Account) (stop bool) { + account := NewGenesisAccountI(acc) + accounts = append(accounts, account) + return false + } + app.accountMapper.IterateAccounts(ctx, appendAccount) + fileAccounts := []GenesisFileAccount{} + for _, acc := range accounts { + var coinsString []string + for _, coin := range acc.Coins { + coinsString = append(coinsString, coin.String()) + } + fileAccounts = append(fileAccounts, + GenesisFileAccount{ + Address: acc.Address, + Coins: coinsString, + Sequence: acc.Sequence, + AccountNumber: acc.AccountNumber, + }) + } + + genState := NewGenesisFileState( + fileAccounts, + auth.ExportGenesis(ctx, app.feeCollectionKeeper), + stake.ExportGenesis(ctx, app.stakeKeeper), + mint.ExportGenesis(ctx, app.mintKeeper), + distr.ExportGenesis(ctx, app.distrKeeper), + gov.ExportGenesis(ctx, app.govKeeper), + upgrade.WriteGenesis(ctx), + service.ExportGenesis(ctx, app.serviceKeeper), + arbitration.ExportGenesis(ctx), + guardian.ExportGenesis(ctx, app.guardianKeeper), + slashing.ExportGenesis(ctx, app.slashingKeeper), + ) + appState, err = codec.MarshalJSONIndent(app.cdc, genState) + if err != nil { + return nil, nil, err + } + validators = stake.WriteValidators(ctx, app.stakeKeeper) + return appState, validators, nil +} + +// prepare for fresh start at zero height +func (app *IrisApp) prepForZeroHeightGenesis(ctx sdk.Context) { + + /* Just to be safe, assert the invariants on current state. */ + app.assertRuntimeInvariantsOnContext(ctx) + + /* Handle fee distribution state. */ + + // withdraw all delegator & validator rewards + vdiIter := func(_ int64, valInfo distr.ValidatorDistInfo) (stop bool) { + _, _, err := app.distrKeeper.WithdrawValidatorRewardsAll(ctx, valInfo.OperatorAddr) + if err != nil { + panic(err) + } + return false + } + app.distrKeeper.IterateValidatorDistInfos(ctx, vdiIter) + + ddiIter := func(_ int64, distInfo distr.DelegationDistInfo) (stop bool) { + _, err := app.distrKeeper.WithdrawDelegationReward( + ctx, distInfo.DelegatorAddr, distInfo.ValOperatorAddr) + if err != nil { + panic(err) + } + return false + } + app.distrKeeper.IterateDelegationDistInfos(ctx, ddiIter) + + app.assertRuntimeInvariantsOnContext(ctx) + + // set distribution info withdrawal heights to 0 + app.distrKeeper.IterateDelegationDistInfos(ctx, func(_ int64, delInfo distr.DelegationDistInfo) (stop bool) { + delInfo.DelPoolWithdrawalHeight = 0 + app.distrKeeper.SetDelegationDistInfo(ctx, delInfo) + return false + }) + app.distrKeeper.IterateValidatorDistInfos(ctx, func(_ int64, valInfo distr.ValidatorDistInfo) (stop bool) { + valInfo.FeePoolWithdrawalHeight = 0 + app.distrKeeper.SetValidatorDistInfo(ctx, valInfo) + return false + }) + + // assert that the fee pool is empty + feePool := app.distrKeeper.GetFeePool(ctx) + if !feePool.TotalValAccum.Accum.IsZero() { + panic("unexpected leftover validator accum") + } + bondDenom := app.stakeKeeper.GetParams(ctx).BondDenom + if !feePool.ValPool.AmountOf(bondDenom).IsZero() { + panic(fmt.Sprintf("unexpected leftover validator pool coins: %v", + feePool.ValPool.AmountOf(bondDenom).String())) + } + + // reset fee pool height, save fee pool + feePool.TotalValAccum = distr.NewTotalAccum(0) + app.distrKeeper.SetFeePool(ctx, feePool) + + /* Handle stake state. */ + + // iterate through validators by power descending, reset bond height, update bond intra-tx counter + store := ctx.KVStore(app.keyStake) + iter := sdk.KVStoreReversePrefixIterator(store, stake.ValidatorsByPowerIndexKey) + counter := int16(0) + for ; iter.Valid(); iter.Next() { + addr := sdk.ValAddress(iter.Value()) + validator, found := app.stakeKeeper.GetValidator(ctx, addr) + if !found { + panic("expected validator, not found") + } + validator.BondHeight = 0 + validator.BondIntraTxCounter = counter + validator.UnbondingHeight = 0 + app.stakeKeeper.SetValidator(ctx, validator) + counter++ + } + iter.Close() + + /* Handle slashing state. */ + + // we have to clear the slashing periods, since they reference heights + app.slashingKeeper.DeleteValidatorSlashingPeriods(ctx) + + // reset start height on signing infos + app.slashingKeeper.IterateValidatorSigningInfos(ctx, func(addr sdk.ConsAddress, info slashing.ValidatorSigningInfo) (stop bool) { + info.StartHeight = 0 + app.slashingKeeper.SetValidatorSigningInfo(ctx, addr, info) + return false + }) +} diff --git a/app/invariants.go b/app/invariants.go new file mode 100644 index 000000000..1d3043fb0 --- /dev/null +++ b/app/invariants.go @@ -0,0 +1,41 @@ +package app + +import ( + "fmt" + "time" + + sdk "github.com/irisnet/irishub/types" + banksim "github.com/irisnet/irishub/modules/bank/simulation" + distrsim "github.com/irisnet/irishub/modules/distribution/simulation" + "github.com/irisnet/irishub/modules/mock/simulation" + stakesim "github.com/irisnet/irishub/modules/stake/simulation" + abci "github.com/tendermint/tendermint/abci/types" +) + +func (app *IrisApp) runtimeInvariants() []simulation.Invariant { + return []simulation.Invariant{ + banksim.NonnegativeBalanceInvariant(app.accountMapper), + distrsim.ValAccumInvariants(app.distrKeeper, app.stakeKeeper), + stakesim.SupplyInvariants(app.bankKeeper, app.stakeKeeper, + app.feeCollectionKeeper, app.distrKeeper, app.serviceKeeper, app.accountMapper), + stakesim.PositivePowerInvariant(app.stakeKeeper), + } +} + +func (app *IrisApp) assertRuntimeInvariants() { + ctx := app.NewContext(false, abci.Header{Height: app.LastBlockHeight() + 1}) + app.assertRuntimeInvariantsOnContext(ctx) +} + +func (app *IrisApp) assertRuntimeInvariantsOnContext(ctx sdk.Context) { + start := time.Now() + invariants := app.runtimeInvariants() + for _, inv := range invariants { + if err := inv(ctx); err != nil { + panic(fmt.Errorf("invariant broken: %s", err)) + } + } + end := time.Now() + diff := end.Sub(start) + app.BaseApp.Logger.With("module", "invariants").Info("Asserted all invariants", "duration", diff) +} diff --git a/app/sim_test.go b/app/sim_test.go index 8f9421b75..93f6bd6ee 100644 --- a/app/sim_test.go +++ b/app/sim_test.go @@ -22,6 +22,7 @@ import ( "github.com/irisnet/irishub/modules/gov" banksim "github.com/irisnet/irishub/modules/bank/simulation" govsim "github.com/irisnet/irishub/modules/gov/simulation" + distrsim "github.com/irisnet/irishub/modules/distribution/simulation" "github.com/irisnet/irishub/modules/mock/simulation" slashingsim "github.com/irisnet/irishub/modules/slashing/simulation" stakesim "github.com/irisnet/irishub/modules/stake/simulation" @@ -35,6 +36,7 @@ var ( enabled bool verbose bool commit bool + period int ) func init() { @@ -44,6 +46,7 @@ func init() { flag.BoolVar(&enabled, "SimulationEnabled", true, "Enable the simulation") flag.BoolVar(&verbose, "SimulationVerbose", false, "Verbose log output") flag.BoolVar(&commit, "SimulationCommit", false, "Have the simulation commit") + flag.IntVar(&period, "SimulationPeriod", 100, "Run slow invariants only once every period assertions") } func appStateFn(r *rand.Rand, accs []simulation.Account) json.RawMessage { @@ -104,7 +107,7 @@ func appStateFn(r *rand.Rand, accs []simulation.Account) json.RawMessage { validators = append(validators, validator) delegations = append(delegations, delegation) } - stakeGenesis.Pool.LooseTokens = sdk.NewDecFromInt(sdk.NewIntWithDecimal(100, 30)) + stakeGenesis.Pool.LooseTokens = sdk.NewDecFromInt(amount.MulRaw(numAccs).Add(amount.MulRaw(numInitiallyBonded))) stakeGenesis.Validators = validators stakeGenesis.Bonds = delegations @@ -141,7 +144,14 @@ func testAndRunTxs(app *IrisApp) []simulation.WeightedOperation { } func invariants(app *IrisApp) []simulation.Invariant { - return []simulation.Invariant{} + return []simulation.Invariant{ + simulation.PeriodicInvariant(banksim.NonnegativeBalanceInvariant(app.accountMapper), period, 0), + simulation.PeriodicInvariant(govsim.AllInvariants(), period, 0), + simulation.PeriodicInvariant(distrsim.AllInvariants(app.distrKeeper, app.stakeKeeper), period, 0), + simulation.PeriodicInvariant(stakesim.AllInvariants(app.bankKeeper, app.stakeKeeper, + app.feeCollectionKeeper, app.distrKeeper, app.serviceKeeper, app.accountMapper), period, 0), + simulation.PeriodicInvariant(slashingsim.AllInvariants(), period, 0), + } } func BenchmarkFullIrisSimulation(b *testing.B) { @@ -159,11 +169,10 @@ func BenchmarkFullIrisSimulation(b *testing.B) { // Run randomized simulation // TODO parameterize numbers, save for a later PR - err := simulation.SimulateFromSeed( + _, err := simulation.SimulateFromSeed( b, app.BaseApp, appStateFn, seed, testAndRunTxs(app), - []simulation.RandSetup{}, - invariants(app), // these shouldn't get ran + invariants(app), numBlocks, blockSize, commit, @@ -202,10 +211,9 @@ func TestFullIrisSimulation(t *testing.T) { require.Equal(t, "IrisApp", app.Name()) // Run randomized simulation - err := simulation.SimulateFromSeed( + _, err := simulation.SimulateFromSeed( t, app.BaseApp, appStateFn, seed, testAndRunTxs(app), - []simulation.RandSetup{}, invariants(app), numBlocks, blockSize, @@ -244,10 +252,9 @@ func TestIrisImportExport(t *testing.T) { require.Equal(t, "IrisApp", app.Name()) // Run randomized simulation - err := simulation.SimulateFromSeed( + _, err := simulation.SimulateFromSeed( t, app.BaseApp, appStateFn, seed, testAndRunTxs(app), - []simulation.RandSetup{}, invariants(app), numBlocks, blockSize, @@ -264,7 +271,7 @@ func TestIrisImportExport(t *testing.T) { fmt.Printf("Exporting genesis...\n") - appState, _, err := app.ExportAppStateAndValidators() + appState, _, err := app.ExportAppStateAndValidators(true) if err != nil { panic(err) } @@ -340,7 +347,6 @@ func TestAppStateDeterminism(t *testing.T) { simulation.SimulateFromSeed( t, app.BaseApp, appStateFn, seed, testAndRunTxs(app), - []simulation.RandSetup{}, []simulation.Invariant{}, 50, 100, diff --git a/baseapp/replay.go b/baseapp/replay.go index af8ada573..a7d4a2135 100644 --- a/baseapp/replay.go +++ b/baseapp/replay.go @@ -1,17 +1,26 @@ package baseapp import ( + "bufio" "fmt" + "os" + "strings" + "github.com/irisnet/irishub/server" "github.com/spf13/viper" bc "github.com/tendermint/tendermint/blockchain" tmcli "github.com/tendermint/tendermint/libs/cli" + cmn "github.com/tendermint/tendermint/libs/common" dbm "github.com/tendermint/tendermint/libs/db" "github.com/tendermint/tendermint/libs/log" "github.com/tendermint/tendermint/node" sm "github.com/tendermint/tendermint/state" ) +const ( + DefaultSyncableHeight = 10000 +) + func Replay(logger log.Logger) int64 { ctx := server.NewDefaultContext() ctx.Config.RootDir = viper.GetString(tmcli.HomeFlag) @@ -51,3 +60,25 @@ func Replay(logger log.Logger) int64 { return loadHeight } + +func ReplayToHeight(replayHeight int64, logger log.Logger) int64 { + loadHeight := int64(0) + logger.Info("Please make sure the replay height is less than block height") + if replayHeight >= DefaultSyncableHeight { + loadHeight = replayHeight - replayHeight%DefaultSyncableHeight + } else { + // version 1 will always be kept + loadHeight = 1 + } + logger.Info("This replay operation will change the application store, please spare your node home directory first") + logger.Info("Confirm that:(y/n)") + input, err := bufio.NewReader(os.Stdin).ReadString('\n') + if err != nil { + cmn.Exit(err.Error()) + } + confirm := strings.TrimSpace(input) + if confirm != "y" && confirm != "yes" { + cmn.Exit("Abort replay operation") + } + return loadHeight +} diff --git a/cmd/iris/main.go b/cmd/iris/main.go index 53d0762c7..9aeb52e67 100644 --- a/cmd/iris/main.go +++ b/cmd/iris/main.go @@ -79,7 +79,7 @@ func newApp(logger log.Logger, db dbm.DB, traceStore io.Writer) abci.Application } func exportAppStateAndTMValidators( - logger log.Logger, db dbm.DB, traceStore io.Writer, height int64, + logger log.Logger, db dbm.DB, traceStore io.Writer, height int64, forZeroHeight bool, ) (json.RawMessage, []tmtypes.GenesisValidator, error) { gApp := app.NewIrisApp(logger, db, traceStore) if height != -1 { @@ -88,5 +88,5 @@ func exportAppStateAndTMValidators( return nil, nil, err } } - return gApp.ExportAppStateAndValidators() + return gApp.ExportAppStateAndValidators(forZeroHeight) } diff --git a/modules/bank/simulation/invariants.go b/modules/bank/simulation/invariants.go index 6705aac30..76cf53a86 100644 --- a/modules/bank/simulation/invariants.go +++ b/modules/bank/simulation/invariants.go @@ -8,15 +8,11 @@ import ( "github.com/irisnet/irishub/modules/auth" "github.com/irisnet/irishub/modules/mock" "github.com/irisnet/irishub/modules/mock/simulation" - abci "github.com/tendermint/tendermint/abci/types" - - "github.com/irisnet/irishub/baseapp" ) // NonnegativeBalanceInvariant checks that all accounts in the application have non-negative balances func NonnegativeBalanceInvariant(mapper auth.AccountKeeper) simulation.Invariant { - return func(app *baseapp.BaseApp) error { - ctx := app.NewContext(false, abci.Header{}) + return func(ctx sdk.Context) error { accts := mock.GetAllAccounts(mapper, ctx) for _, acc := range accts { coins := acc.GetCoins() @@ -33,8 +29,7 @@ func NonnegativeBalanceInvariant(mapper auth.AccountKeeper) simulation.Invariant // TotalCoinsInvariant checks that the sum of the coins across all accounts // is what is expected func TotalCoinsInvariant(mapper auth.AccountKeeper, totalSupplyFn func() sdk.Coins) simulation.Invariant { - return func(app *baseapp.BaseApp) error { - ctx := app.NewContext(false, abci.Header{}) + return func(ctx sdk.Context) error { totalCoins := sdk.Coins{} chkAccount := func(acc auth.Account) bool { diff --git a/modules/bank/simulation/sim_test.go b/modules/bank/simulation/sim_test.go deleted file mode 100644 index de42eb644..000000000 --- a/modules/bank/simulation/sim_test.go +++ /dev/null @@ -1,47 +0,0 @@ -package simulation - -import ( - "encoding/json" - "math/rand" - "testing" - - sdk "github.com/irisnet/irishub/types" - "github.com/irisnet/irishub/modules/bank" - "github.com/irisnet/irishub/modules/mock" - "github.com/irisnet/irishub/modules/mock/simulation" - stakeTypes "github.com/irisnet/irishub/modules/stake/types" -) - -func TestBankWithRandomMessages(t *testing.T) { - mapp := mock.NewApp() - - bank.RegisterCodec(mapp.Cdc) - mapper := mapp.AccountKeeper - bankKeeper := mapp.BankKeeper - - mapp.Router().AddRoute("bank", []*sdk.KVStoreKey{mapp.KeyAccount}, bank.NewHandler(bankKeeper)) - - err := mapp.CompleteSetup() - if err != nil { - panic(err) - } - - appStateFn := func(r *rand.Rand, accs []simulation.Account) json.RawMessage { - simulation.RandomSetGenesis(r, mapp, accs, []string{stakeTypes.StakeDenom}) - return json.RawMessage("{}") - } - - simulation.Simulate( - t, mapp.BaseApp, appStateFn, - []simulation.WeightedOperation{ - {1, SingleInputSendMsg(mapper, bankKeeper)}, - }, - []simulation.RandSetup{}, - []simulation.Invariant{ - NonnegativeBalanceInvariant(mapper), - TotalCoinsInvariant(mapper, func() sdk.Coins { return mapp.TotalCoinsSupply }), - }, - 30, 60, - false, - ) -} diff --git a/modules/distribution/alias.go b/modules/distribution/alias.go index 902bd5307..5a290a5b8 100644 --- a/modules/distribution/alias.go +++ b/modules/distribution/alias.go @@ -56,6 +56,8 @@ var ( NewMsgWithdrawDelegatorRewardsAll = types.NewMsgWithdrawDelegatorRewardsAll NewMsgWithdrawDelegatorReward = types.NewMsgWithdrawDelegatorReward NewMsgWithdrawValidatorRewardsAll = types.NewMsgWithdrawValidatorRewardsAll + + NewTotalAccum = types.NewTotalAccum ) const ( diff --git a/modules/distribution/keeper/delegation.go b/modules/distribution/keeper/delegation.go index 73291e092..ccae60e8d 100644 --- a/modules/distribution/keeper/delegation.go +++ b/modules/distribution/keeper/delegation.go @@ -43,6 +43,34 @@ func (k Keeper) RemoveDelegationDistInfo(ctx sdk.Context, delAddr sdk.AccAddress store.Delete(GetDelegationDistInfoKey(delAddr, valOperatorAddr)) } +// remove all delegation distribution infos +func (k Keeper) RemoveDelegationDistInfos(ctx sdk.Context) { + store := ctx.KVStore(k.storeKey) + iter := sdk.KVStorePrefixIterator(store, DelegationDistInfoKey) + defer iter.Close() + for ; iter.Valid(); iter.Next() { + store.Delete(iter.Key()) + } +} + +// iterate over all the validator distribution infos +func (k Keeper) IterateDelegationDistInfos(ctx sdk.Context, + fn func(index int64, distInfo types.DelegationDistInfo) (stop bool)) { + + store := ctx.KVStore(k.storeKey) + iter := sdk.KVStorePrefixIterator(store, DelegationDistInfoKey) + defer iter.Close() + index := int64(0) + for ; iter.Valid(); iter.Next() { + var ddi types.DelegationDistInfo + k.cdc.MustUnmarshalBinaryLengthPrefixed(iter.Value(), &ddi) + if fn(index, ddi) { + return + } + index++ + } +} + //___________________________________________________________________________________________ // get the delegator withdraw address, return the delegator address if not set diff --git a/modules/distribution/simulation/invariants.go b/modules/distribution/simulation/invariants.go index 9503981ed..37b900b5e 100644 --- a/modules/distribution/simulation/invariants.go +++ b/modules/distribution/simulation/invariants.go @@ -3,18 +3,26 @@ package simulation import ( "fmt" - "github.com/irisnet/irishub/baseapp" sdk "github.com/irisnet/irishub/types" distr "github.com/irisnet/irishub/modules/distribution" "github.com/irisnet/irishub/modules/mock/simulation" - abci "github.com/tendermint/tendermint/abci/types" + "github.com/irisnet/irishub/modules/stake" ) // AllInvariants runs all invariants of the distribution module // Currently: total supply, positive power -func AllInvariants(d distr.Keeper, sk distr.StakeKeeper) simulation.Invariant { - return func(app *baseapp.BaseApp) error { - err := ValAccumInvariants(d, sk)(app) +func AllInvariants(d distr.Keeper, stk stake.Keeper) simulation.Invariant { + sk := distr.StakeKeeper(stk) + return func(ctx sdk.Context) error { + err := ValAccumInvariants(d, sk)(ctx) + if err != nil { + return err + } + err = DelAccumInvariants(d, sk)(ctx) + if err != nil { + return err + } + err = CanWithdrawInvariant(d, stk)(ctx) if err != nil { return err } @@ -25,9 +33,7 @@ func AllInvariants(d distr.Keeper, sk distr.StakeKeeper) simulation.Invariant { // ValAccumInvariants checks that the fee pool accum == sum all validators' accum func ValAccumInvariants(k distr.Keeper, sk distr.StakeKeeper) simulation.Invariant { - return func(app *baseapp.BaseApp) error { - mockHeader := abci.Header{Height: app.LastBlockHeight() + 1} - ctx := app.NewContext(false, mockHeader) + return func(ctx sdk.Context) error { height := ctx.BlockHeight() valAccum := sdk.ZeroDec() @@ -48,3 +54,121 @@ func ValAccumInvariants(k distr.Keeper, sk distr.StakeKeeper) simulation.Invaria return nil } } + +// DelAccumInvariants checks that each validator del accum == sum all delegators' accum +func DelAccumInvariants(k distr.Keeper, sk distr.StakeKeeper) simulation.Invariant { + + return func(ctx sdk.Context) error { + height := ctx.BlockHeight() + + totalDelAccumFromVal := make(map[string]sdk.Dec) // key is the valOpAddr string + totalDelAccum := make(map[string]sdk.Dec) + + // iterate the validators + iterVal := func(_ int64, vdi distr.ValidatorDistInfo) bool { + key := vdi.OperatorAddr.String() + validator := sk.Validator(ctx, vdi.OperatorAddr) + totalDelAccumFromVal[key] = vdi.GetTotalDelAccum(height, + validator.GetDelegatorShares()) + + // also initialize the delegation map + totalDelAccum[key] = sdk.ZeroDec() + + return false + } + k.IterateValidatorDistInfos(ctx, iterVal) + + // iterate the delegations + iterDel := func(_ int64, ddi distr.DelegationDistInfo) bool { + key := ddi.ValOperatorAddr.String() + delegation := sk.Delegation(ctx, ddi.DelegatorAddr, ddi.ValOperatorAddr) + totalDelAccum[key] = totalDelAccum[key].Add( + ddi.GetDelAccum(height, delegation.GetShares())) + return false + } + k.IterateDelegationDistInfos(ctx, iterDel) + + // compare + for key, delAccumFromVal := range totalDelAccumFromVal { + sumDelAccum := totalDelAccum[key] + + if !sumDelAccum.Equal(delAccumFromVal) { + + logDelAccums := "" + iterDel := func(_ int64, ddi distr.DelegationDistInfo) bool { + keyLog := ddi.ValOperatorAddr.String() + if keyLog == key { + delegation := sk.Delegation(ctx, ddi.DelegatorAddr, ddi.ValOperatorAddr) + accum := ddi.GetDelAccum(height, delegation.GetShares()) + if accum.IsPositive() { + logDelAccums += fmt.Sprintf("\n\t\tdel: %v, accum: %v", + ddi.DelegatorAddr.String(), + accum.String()) + } + } + return false + } + k.IterateDelegationDistInfos(ctx, iterDel) + + operAddr, err := sdk.ValAddressFromBech32(key) + if err != nil { + panic(err) + } + validator := sk.Validator(ctx, operAddr) + + return fmt.Errorf("delegator accum invariance: \n"+ + "\tvalidator key: %v\n"+ + "\tvalidator: %+v\n"+ + "\tsum delegator accum: %v\n"+ + "\tvalidator's total delegator accum: %v\n"+ + "\tlog of delegations with accum: %v\n", + key, validator, sumDelAccum.String(), + delAccumFromVal.String(), logDelAccums) + } + } + + return nil + } +} + +// CanWithdrawInvariant checks that current rewards can be completely withdrawn +func CanWithdrawInvariant(k distr.Keeper, sk stake.Keeper) simulation.Invariant { + return func(ctx sdk.Context) error { + // we don't want to write the changes + ctx, _ = ctx.CacheContext() + + // withdraw all delegator & validator rewards + vdiIter := func(_ int64, valInfo distr.ValidatorDistInfo) (stop bool) { + _, _, err := k.WithdrawValidatorRewardsAll(ctx, valInfo.OperatorAddr) + if err != nil { + panic(err) + } + return false + } + k.IterateValidatorDistInfos(ctx, vdiIter) + + ddiIter := func(_ int64, distInfo distr.DelegationDistInfo) (stop bool) { + _, err := k.WithdrawDelegationReward( + ctx, distInfo.DelegatorAddr, distInfo.ValOperatorAddr) + if err != nil { + panic(err) + } + return false + } + k.IterateDelegationDistInfos(ctx, ddiIter) + + // assert that the fee pool is empty + feePool := k.GetFeePool(ctx) + if !feePool.TotalValAccum.Accum.IsZero() { + return fmt.Errorf("unexpected leftover validator accum") + } + bondDenom := sk.GetParams(ctx).BondDenom + if !feePool.ValPool.AmountOf(bondDenom).IsZero() { + return fmt.Errorf("unexpected leftover validator pool coins: %v", + feePool.ValPool.AmountOf(bondDenom).String()) + } + + // all ok + return nil + } +} diff --git a/modules/gov/simulation/invariants.go b/modules/gov/simulation/invariants.go index c924954d3..9beb6d1a8 100644 --- a/modules/gov/simulation/invariants.go +++ b/modules/gov/simulation/invariants.go @@ -1,13 +1,13 @@ package simulation import ( - "github.com/irisnet/irishub/baseapp" + sdk "github.com/irisnet/irishub/types" "github.com/irisnet/irishub/modules/mock/simulation" ) // AllInvariants tests all governance invariants func AllInvariants() simulation.Invariant { - return func(app *baseapp.BaseApp) error { + return func(ctx sdk.Context) error { // TODO Add some invariants! // Checking proposal queues, no passed-but-unexecuted proposals, etc. return nil diff --git a/modules/gov/simulation/sim_test.go b/modules/gov/simulation/sim_test.go deleted file mode 100644 index 868c2cc1c..000000000 --- a/modules/gov/simulation/sim_test.go +++ /dev/null @@ -1,96 +0,0 @@ -package simulation - -import ( - "encoding/json" - "math/rand" - "testing" - - abci "github.com/tendermint/tendermint/abci/types" - - sdk "github.com/irisnet/irishub/types" - "github.com/irisnet/irishub/modules/bank" - "github.com/irisnet/irishub/modules/stake" - "github.com/irisnet/irishub/modules/gov" - "github.com/irisnet/irishub/modules/mock" - "github.com/irisnet/irishub/modules/mock/simulation" -) - -// TestGovWithRandomMessages -func TestGovWithRandomMessages(t *testing.T) { - mapp := mock.NewApp() - - bank.RegisterCodec(mapp.Cdc) - gov.RegisterCodec(mapp.Cdc) - - bankKeeper := mapp.BankKeeper - stakeKey := mapp.KeyStake - stakeTKey := mapp.TkeyStake - paramKey := mapp.KeyParams - govKey := sdk.NewKVStoreKey("gov") - - paramKeeper := mapp.ParamsKeeper - stakeKeeper := stake.NewKeeper( - mapp.Cdc, stakeKey, - stakeTKey, bankKeeper, - paramKeeper.Subspace(stake.DefaultParamspace), - stake.DefaultCodespace, - ) - govKeeper := gov.NewKeeper( - mapp.Cdc, - govKey, - bankKeeper, stakeKeeper, - gov.DefaultCodespace, - ) - - mapp.Router().AddRoute("gov", []*sdk.KVStoreKey{govKey, mapp.KeyAccount, stakeKey, paramKey}, gov.NewHandler(govKeeper)) - mapp.SetEndBlocker(func(ctx sdk.Context, req abci.RequestEndBlock) abci.ResponseEndBlock { - gov.EndBlocker(ctx, govKeeper) - return abci.ResponseEndBlock{} - }) - - err := mapp.CompleteSetup(govKey) - if err != nil { - panic(err) - } - - appStateFn := func(r *rand.Rand, accs []simulation.Account) json.RawMessage { - simulation.RandomSetGenesis(r, mapp, accs, []string{"stake"}) - return json.RawMessage("{}") - } - - setup := func(r *rand.Rand, accs []simulation.Account) { - ctx := mapp.NewContext(false, abci.Header{}) - stake.InitGenesis(ctx, stakeKeeper, stake.DefaultGenesisState()) - - gov.InitGenesis(ctx, govKeeper, gov.DefaultGenesisState()) - } - - // Test with unscheduled votes - simulation.Simulate( - t, mapp.BaseApp, appStateFn, - []simulation.WeightedOperation{ - {2, SimulateMsgSubmitProposal(govKeeper, stakeKeeper)}, - {3, SimulateMsgDeposit(govKeeper, stakeKeeper)}, - {20, SimulateMsgVote(govKeeper, stakeKeeper)}, - }, []simulation.RandSetup{ - setup, - }, []simulation.Invariant{ - //AllInvariants(), - }, 10, 100, - false, - ) - - // Test with scheduled votes - simulation.Simulate( - t, mapp.BaseApp, appStateFn, - []simulation.WeightedOperation{ - {10, SimulateSubmittingVotingAndSlashingForProposal(govKeeper, stakeKeeper)}, - {5, SimulateMsgDeposit(govKeeper, stakeKeeper)}, - }, []simulation.RandSetup{ - setup, - }, []simulation.Invariant{ - AllInvariants(), - }, 10, 100, - false, - ) -} diff --git a/modules/mock/simulation/account.go b/modules/mock/simulation/account.go new file mode 100644 index 000000000..b41125c1d --- /dev/null +++ b/modules/mock/simulation/account.go @@ -0,0 +1,51 @@ +package simulation + +import ( + "math/rand" + + "github.com/tendermint/tendermint/crypto" + "github.com/tendermint/tendermint/crypto/ed25519" + "github.com/tendermint/tendermint/crypto/secp256k1" + + sdk "github.com/irisnet/irishub/types" +) + +// Account contains a privkey, pubkey, address tuple +// eventually more useful data can be placed in here. +// (e.g. number of coins) +type Account struct { + PrivKey crypto.PrivKey + PubKey crypto.PubKey + Address sdk.AccAddress +} + +// are two accounts equal +func (acc Account) Equals(acc2 Account) bool { + return acc.Address.Equals(acc2.Address) +} + +// RandomAcc pick a random account from an array +func RandomAcc(r *rand.Rand, accs []Account) Account { + return accs[r.Intn( + len(accs), + )] +} + +// RandomAccounts generates n random accounts +func RandomAccounts(r *rand.Rand, n int) []Account { + accs := make([]Account, n) + for i := 0; i < n; i++ { + // don't need that much entropy for simulation + privkeySeed := make([]byte, 15) + r.Read(privkeySeed) + useSecp := r.Int63()%2 == 0 + if useSecp { + accs[i].PrivKey = secp256k1.GenPrivKeySecp256k1(privkeySeed) + } else { + accs[i].PrivKey = ed25519.GenPrivKeyFromSecret(privkeySeed) + } + accs[i].PubKey = accs[i].PrivKey.PubKey() + accs[i].Address = sdk.AccAddress(accs[i].PubKey.Address()) + } + return accs +} diff --git a/modules/mock/simulation/doc.go b/modules/mock/simulation/doc.go new file mode 100644 index 000000000..ff292bd38 --- /dev/null +++ b/modules/mock/simulation/doc.go @@ -0,0 +1,25 @@ +/* +Package simulation implements a simulation framework for any state machine +built on the SDK which utilizes auth. + +It is primarily intended for fuzz testing the integration of modules. It will +test that the provided operations are interoperable, and that the desired +invariants hold. It can additionally be used to detect what the performance +benchmarks in the system are, by using benchmarking mode and cpu / mem +profiling. If it detects a failure, it provides the entire log of what was ran. + +The simulator takes as input: a random seed, the set of operations to run, the +invariants to test, and additional parameters to configure how long to run, and +misc. parameters that affect simulation speed. + +It is intended that every module provides a list of Operations which will +randomly create and run a message / tx in a manner that is interesting to fuzz, +and verify that the state transition was executed as expected. Each module +should additionally provide methods to assert that the desired invariants hold. + +Then to perform a randomized simulation, select the set of desired operations, +the weightings for each, the invariants you want to test, and how long to run +it for. Then run simulation.Simulate! The simulator will handle things like +ensuring that validators periodically double signing, or go offline. +*/ +package simulation diff --git a/modules/mock/simulation/event_stats.go b/modules/mock/simulation/event_stats.go new file mode 100644 index 000000000..f54eef4cc --- /dev/null +++ b/modules/mock/simulation/event_stats.go @@ -0,0 +1,30 @@ +package simulation + +import ( + "fmt" + "sort" +) + +type eventStats map[string]uint + +func newEventStats() eventStats { + events := make(map[string]uint) + return events +} + +func (es eventStats) tally(eventDesc string) { + es[eventDesc]++ +} + +// Pretty-print events as a table +func (es eventStats) Print() { + var keys []string + for key := range es { + keys = append(keys, key) + } + sort.Strings(keys) + fmt.Printf("Event statistics: \n") + for _, key := range keys { + fmt.Printf(" % 60s => %d\n", key, es[key]) + } +} diff --git a/modules/mock/simulation/invariants.go b/modules/mock/simulation/invariants.go new file mode 100644 index 000000000..79b4c27e1 --- /dev/null +++ b/modules/mock/simulation/invariants.go @@ -0,0 +1,34 @@ +package simulation + +import ( + "fmt" + "testing" + + "github.com/irisnet/irishub/baseapp" + sdk "github.com/irisnet/irishub/types" + abci "github.com/tendermint/tendermint/abci/types" +) + +// An Invariant is a function which tests a particular invariant. +// If the invariant has been broken, it should return an error +// containing a descriptive message about what happened. +// The simulator will then halt and print the logs. +type Invariant func(ctx sdk.Context) error + +// group of Invarient +type Invariants []Invariant + +// assertAll asserts the all invariants against application state +func (invs Invariants) assertAll(t *testing.T, app *baseapp.BaseApp, + event string, displayLogs func()) { + + ctx := app.NewContext(false, abci.Header{Height: app.LastBlockHeight() + 1}) + + for i := 0; i < len(invs); i++ { + if err := invs[i](ctx); err != nil { + fmt.Printf("Invariants broken after %s\n%s\n", event, err.Error()) + displayLogs() + t.Fatal() + } + } +} diff --git a/modules/mock/simulation/mock_tendermint.go b/modules/mock/simulation/mock_tendermint.go new file mode 100644 index 000000000..54e38d5c7 --- /dev/null +++ b/modules/mock/simulation/mock_tendermint.go @@ -0,0 +1,202 @@ +package simulation + +import ( + "fmt" + "math/rand" + "sort" + "testing" + "time" + + abci "github.com/tendermint/tendermint/abci/types" + cmn "github.com/tendermint/tendermint/libs/common" + tmtypes "github.com/tendermint/tendermint/types" +) + +type mockValidator struct { + val abci.ValidatorUpdate + livenessState int +} + +type mockValidators map[string]mockValidator + +// get mockValidators from abci validators +func newMockValidators(r *rand.Rand, abciVals []abci.ValidatorUpdate, + params Params) mockValidators { + + validators := make(mockValidators) + for _, validator := range abciVals { + str := fmt.Sprintf("%v", validator.PubKey) + liveliness := GetMemberOfInitialState(r, + params.InitialLivenessWeightings) + + validators[str] = mockValidator{ + val: validator, + livenessState: liveliness, + } + } + + return validators +} + +// TODO describe usage +func (vals mockValidators) getKeys() []string { + keys := make([]string, len(vals)) + i := 0 + for key := range vals { + keys[i] = key + i++ + } + sort.Strings(keys) + return keys +} + +//_________________________________________________________________________________ + +// randomProposer picks a random proposer from the current validator set +func (vals mockValidators) randomProposer(r *rand.Rand) cmn.HexBytes { + keys := vals.getKeys() + if len(keys) == 0 { + return nil + } + key := keys[r.Intn(len(keys))] + proposer := vals[key].val + pk, err := tmtypes.PB2TM.PubKey(proposer.PubKey) + if err != nil { + panic(err) + } + return pk.Address() +} + +// updateValidators mimicks Tendermint's update logic +// nolint: unparam +func updateValidators(tb testing.TB, r *rand.Rand, params Params, + current map[string]mockValidator, updates []abci.ValidatorUpdate, + event func(string)) map[string]mockValidator { + + for _, update := range updates { + str := fmt.Sprintf("%v", update.PubKey) + + if update.Power == 0 { + if _, ok := current[str]; !ok { + tb.Fatalf("tried to delete a nonexistent validator") + } + event("endblock/validatorupdates/kicked") + delete(current, str) + + } else if mVal, ok := current[str]; ok { + // validator already exists + mVal.val = update + event("endblock/validatorupdates/updated") + + } else { + // Set this new validator + current[str] = mockValidator{ + update, + GetMemberOfInitialState(r, params.InitialLivenessWeightings), + } + event("endblock/validatorupdates/added") + } + } + + return current +} + +// RandomRequestBeginBlock generates a list of signing validators according to +// the provided list of validators, signing fraction, and evidence fraction +func RandomRequestBeginBlock(r *rand.Rand, params Params, + validators mockValidators, pastTimes []time.Time, + pastVoteInfos [][]abci.VoteInfo, + event func(string), header abci.Header) abci.RequestBeginBlock { + + if len(validators) == 0 { + return abci.RequestBeginBlock{ + Header: header, + } + } + + voteInfos := make([]abci.VoteInfo, len(validators)) + for i, key := range validators.getKeys() { + mVal := validators[key] + mVal.livenessState = params.LivenessTransitionMatrix.NextState(r, mVal.livenessState) + signed := true + + if mVal.livenessState == 1 { + // spotty connection, 50% probability of success + // See https://github.com/golang/go/issues/23804#issuecomment-365370418 + // for reasoning behind computing like this + signed = r.Int63()%2 == 0 + } else if mVal.livenessState == 2 { + // offline + signed = false + } + + if signed { + event("beginblock/signing/signed") + } else { + event("beginblock/signing/missed") + } + + pubkey, err := tmtypes.PB2TM.PubKey(mVal.val.PubKey) + if err != nil { + panic(err) + } + voteInfos[i] = abci.VoteInfo{ + Validator: abci.Validator{ + Address: pubkey.Address(), + Power: mVal.val.Power, + }, + SignedLastBlock: signed, + } + } + + // return if no past times + if len(pastTimes) <= 0 { + return abci.RequestBeginBlock{ + Header: header, + LastCommitInfo: abci.LastCommitInfo{ + Votes: voteInfos, + }, + } + } + + // TODO: Determine capacity before allocation + evidence := make([]abci.Evidence, 0) + for r.Float64() < params.EvidenceFraction { + + height := header.Height + time := header.Time + vals := voteInfos + + if r.Float64() < params.PastEvidenceFraction && header.Height > 1 { + height = int64(r.Intn(int(header.Height)-1)) + 1 // Tendermint starts at height 1 + // array indices offset by one + time = pastTimes[height-1] + vals = pastVoteInfos[height-1] + } + validator := vals[r.Intn(len(vals))].Validator + + var totalVotingPower int64 + for _, val := range vals { + totalVotingPower += val.Validator.Power + } + + evidence = append(evidence, + abci.Evidence{ + Type: tmtypes.ABCIEvidenceTypeDuplicateVote, + Validator: validator, + Height: height, + Time: time, + TotalVotingPower: totalVotingPower, + }, + ) + event("beginblock/evidence") + } + + return abci.RequestBeginBlock{ + Header: header, + LastCommitInfo: abci.LastCommitInfo{ + Votes: voteInfos, + }, + ByzantineValidators: evidence, + } +} diff --git a/modules/mock/simulation/operation.go b/modules/mock/simulation/operation.go new file mode 100644 index 000000000..bdcba0c75 --- /dev/null +++ b/modules/mock/simulation/operation.go @@ -0,0 +1,112 @@ +package simulation + +import ( + "math/rand" + "sort" + "time" + + "github.com/irisnet/irishub/baseapp" + sdk "github.com/irisnet/irishub/types" +) + +// Operation runs a state machine transition, and ensures the transition +// happened as expected. The operation could be running and testing a fuzzed +// transaction, or doing the same for a message. +// +// For ease of debugging, an operation returns a descriptive message "action", +// which details what this fuzzed state machine transition actually did. +// +// Operations can optionally provide a list of "FutureOperations" to run later +// These will be ran at the beginning of the corresponding block. +type Operation func(r *rand.Rand, app *baseapp.BaseApp, + ctx sdk.Context, accounts []Account, event func(string)) ( + action string, futureOps []FutureOperation, err error) + +// queue of operations +type OperationQueue map[int][]Operation + +func newOperationQueue() OperationQueue { + operationQueue := make(OperationQueue) + return operationQueue +} + +// adds all future operations into the operation queue. +func queueOperations(queuedOps OperationQueue, + queuedTimeOps []FutureOperation, futureOps []FutureOperation) { + + if futureOps == nil { + return + } + + for _, futureOp := range futureOps { + if futureOp.BlockHeight != 0 { + if val, ok := queuedOps[futureOp.BlockHeight]; ok { + queuedOps[futureOp.BlockHeight] = append(val, futureOp.Op) + } else { + queuedOps[futureOp.BlockHeight] = []Operation{futureOp.Op} + } + continue + } + + // TODO: Replace with proper sorted data structure, so don't have the + // copy entire slice + index := sort.Search( + len(queuedTimeOps), + func(i int) bool { + return queuedTimeOps[i].BlockTime.After(futureOp.BlockTime) + }, + ) + queuedTimeOps = append(queuedTimeOps, FutureOperation{}) + copy(queuedTimeOps[index+1:], queuedTimeOps[index:]) + queuedTimeOps[index] = futureOp + } +} + +//________________________________________________________________________ + +// FutureOperation is an operation which will be ran at the beginning of the +// provided BlockHeight. If both a BlockHeight and BlockTime are specified, it +// will use the BlockHeight. In the (likely) event that multiple operations +// are queued at the same block height, they will execute in a FIFO pattern. +type FutureOperation struct { + BlockHeight int + BlockTime time.Time + Op Operation +} + +//________________________________________________________________________ + +// WeightedOperation is an operation with associated weight. +// This is used to bias the selection operation within the simulator. +type WeightedOperation struct { + Weight int + Op Operation +} + +// WeightedOperations is the group of all weighted operations to simulate. +type WeightedOperations []WeightedOperation + +func (ops WeightedOperations) totalWeight() int { + totalOpWeight := 0 + for _, op := range ops { + totalOpWeight += op.Weight + } + return totalOpWeight +} + +type selectOpFn func(r *rand.Rand) Operation + +func (ops WeightedOperations) getSelectOpFn() selectOpFn { + totalOpWeight := ops.totalWeight() + return func(r *rand.Rand) Operation { + x := r.Intn(totalOpWeight) + for i := 0; i < len(ops); i++ { + if x <= ops[i].Weight { + return ops[i].Op + } + x -= ops[i].Weight + } + // shouldn't happen + return ops[0].Op + } +} diff --git a/modules/mock/simulation/params.go b/modules/mock/simulation/params.go index 5d7e2f264..8499e6c11 100644 --- a/modules/mock/simulation/params.go +++ b/modules/mock/simulation/params.go @@ -1,6 +1,8 @@ package simulation -import "math/rand" +import ( + "math/rand" +) const ( // Minimum time per block @@ -13,15 +15,18 @@ const ( onOperation bool = false ) +// TODO explain transitional matrix usage var ( - // Currently there are 3 different liveness types, fully online, spotty connection, offline. + // Currently there are 3 different liveness types, + // fully online, spotty connection, offline. defaultLivenessTransitionMatrix, _ = CreateTransitionMatrix([][]int{ {90, 20, 1}, {10, 50, 5}, {0, 10, 1000}, }) - // 3 states: rand in range [0, 4*provided blocksize], rand in range [0, 2 * provided blocksize], 0 + // 3 states: rand in range [0, 4*provided blocksize], + // rand in range [0, 2 * provided blocksize], 0 defaultBlockSizeTransitionMatrix, _ = CreateTransitionMatrix([][]int{ {85, 5, 0}, {15, 92, 1}, diff --git a/modules/mock/simulation/rand_util.go b/modules/mock/simulation/rand_util.go new file mode 100644 index 000000000..0c792594e --- /dev/null +++ b/modules/mock/simulation/rand_util.go @@ -0,0 +1,85 @@ +package simulation + +import ( + "math/big" + "math/rand" + "time" + + sdk "github.com/irisnet/irishub/types" + "github.com/irisnet/irishub/modules/mock" +) + +const ( + letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + letterIdxBits = 6 // 6 bits to represent a letter index + letterIdxMask = 1<= 0; { + if remain == 0 { + cache, remain = r.Int63(), letterIdxMax + } + if idx := int(cache & letterIdxMask); idx < len(letterBytes) { + b[i] = letterBytes[idx] + i-- + } + cache >>= letterIdxBits + remain-- + } + return string(b) +} + +// RandomDecAmount generates a random decimal amount +func RandomDecAmount(r *rand.Rand, max sdk.Dec) sdk.Dec { + randInt := big.NewInt(0).Rand(r, max.Int) + return sdk.NewDecFromBigIntWithPrec(randInt, sdk.Precision) +} + +// RandomSetGenesis wraps mock.RandomSetGenesis, but using simulation accounts +func RandomSetGenesis(r *rand.Rand, app *mock.App, accs []Account, denoms []string) { + addrs := make([]sdk.AccAddress, len(accs)) + for i := 0; i < len(accs); i++ { + addrs[i] = accs[i].Address + } + mock.RandomSetGenesis(r, app, addrs, denoms) +} + +// RandTimestamp generates a random timestamp +func RandTimestamp(r *rand.Rand) time.Time { + // json.Marshal breaks for timestamps greater with year greater than 9999 + unixTime := r.Int63n(253373529600) + return time.Unix(unixTime, 0) +} + +// Derive a new rand deterministically from a rand. +// Unlike rand.New(rand.NewSource(seed)), the result is "more random" +// depending on the source and state of r. +// NOTE: not crypto safe. +func DeriveRand(r *rand.Rand) *rand.Rand { + const num = 8 // TODO what's a good number? Too large is too slow. + ms := multiSource(make([]rand.Source, num)) + for i := 0; i < num; i++ { + ms[i] = rand.NewSource(r.Int63()) + } + return rand.New(ms) +} + +type multiSource []rand.Source + +func (ms multiSource) Int63() (r int64) { + for _, source := range ms { + r ^= source.Int63() + } + return r +} + +func (ms multiSource) Seed(seed int64) { + panic("multiSource Seed should not be called") +} diff --git a/modules/mock/simulation/random_simulate_blocks.go b/modules/mock/simulation/random_simulate_blocks.go deleted file mode 100644 index 2a818ed43..000000000 --- a/modules/mock/simulation/random_simulate_blocks.go +++ /dev/null @@ -1,492 +0,0 @@ -package simulation - -import ( - "encoding/json" - "fmt" - "math/rand" - "os" - "os/signal" - "runtime/debug" - "sort" - "strings" - "syscall" - "testing" - "time" - - sdk "github.com/irisnet/irishub/types" - abci "github.com/tendermint/tendermint/abci/types" - cmn "github.com/tendermint/tendermint/libs/common" - tmtypes "github.com/tendermint/tendermint/types" - - bam "github.com/irisnet/irishub/baseapp" -) - -// Simulate tests application by sending random messages. -func Simulate(t *testing.T, app *bam.BaseApp, - appStateFn func(r *rand.Rand, accs []Account) json.RawMessage, - ops []WeightedOperation, setups []RandSetup, - invariants []Invariant, numBlocks int, blockSize int, commit bool) error { - - time := time.Now().UnixNano() - return SimulateFromSeed(t, app, appStateFn, time, ops, setups, invariants, numBlocks, blockSize, commit) -} - -func initChain(r *rand.Rand, params Params, accounts []Account, setups []RandSetup, app *bam.BaseApp, - appStateFn func(r *rand.Rand, accounts []Account) json.RawMessage) (validators map[string]mockValidator) { - res := app.InitChain(abci.RequestInitChain{AppStateBytes: appStateFn(r, accounts)}) - validators = make(map[string]mockValidator) - for _, validator := range res.Validators { - str := fmt.Sprintf("%v", validator.PubKey) - validators[str] = mockValidator{ - val: validator, - livenessState: GetMemberOfInitialState(r, params.InitialLivenessWeightings), - } - } - - for i := 0; i < len(setups); i++ { - setups[i](r, accounts) - } - - return -} - -func randTimestamp(r *rand.Rand) time.Time { - // json.Marshal breaks for timestamps greater with year greater than 9999 - unixTime := r.Int63n(253373529600) - return time.Unix(unixTime, 0) -} - -// SimulateFromSeed tests an application by running the provided -// operations, testing the provided invariants, but using the provided seed. -func SimulateFromSeed(tb testing.TB, app *bam.BaseApp, - appStateFn func(r *rand.Rand, accs []Account) json.RawMessage, - seed int64, ops []WeightedOperation, setups []RandSetup, invariants []Invariant, - numBlocks int, blockSize int, commit bool) (simError error) { - - // in case we have to end early, don't os.Exit so that we can run cleanup code. - stopEarly := false - testingMode, t, b := getTestingMode(tb) - fmt.Printf("Starting SimulateFromSeed with randomness created with seed %d\n", int(seed)) - r := rand.New(rand.NewSource(seed)) - params := RandomParams(r) // := DefaultParams() - fmt.Printf("Randomized simulation params: %+v\n", params) - timestamp := randTimestamp(r) - fmt.Printf("Starting the simulation from time %v, unixtime %v\n", timestamp.UTC().Format(time.UnixDate), timestamp.Unix()) - timeDiff := maxTimePerBlock - minTimePerBlock - - accs := RandomAccounts(r, params.NumKeys) - - // Setup event stats - events := make(map[string]uint) - event := func(what string) { - events[what]++ - } - - validators := initChain(r, params, accs, setups, app, appStateFn) - // Second variable to keep pending validator set (delayed one block since TM 0.24) - // Initially this is the same as the initial validator set - nextValidators := validators - - header := abci.Header{Height: 1, Time: timestamp, ProposerAddress: randomProposer(r, validators)} - opCount := 0 - - // Setup code to catch SIGTERM's - c := make(chan os.Signal) - signal.Notify(c, os.Interrupt, syscall.SIGTERM, syscall.SIGINT) - go func() { - receivedSignal := <-c - fmt.Printf("\nExiting early due to %s, on block %d, operation %d\n", receivedSignal, header.Height, opCount) - simError = fmt.Errorf("Exited due to %s", receivedSignal) - stopEarly = true - }() - - var pastTimes []time.Time - var pastVoteInfos [][]abci.VoteInfo - - request := RandomRequestBeginBlock(r, params, validators, pastTimes, pastVoteInfos, event, header) - // These are operations which have been queued by previous operations - operationQueue := make(map[int][]Operation) - timeOperationQueue := []FutureOperation{} - var blockLogBuilders []*strings.Builder - - if testingMode { - blockLogBuilders = make([]*strings.Builder, numBlocks) - } - displayLogs := logPrinter(testingMode, blockLogBuilders) - blockSimulator := createBlockSimulator(testingMode, tb, t, params, event, invariants, ops, operationQueue, timeOperationQueue, numBlocks, blockSize, displayLogs) - if !testingMode { - b.ResetTimer() - } else { - // Recover logs in case of panic - defer func() { - if r := recover(); r != nil { - fmt.Println("Panic with err\n", r) - stackTrace := string(debug.Stack()) - fmt.Println(stackTrace) - displayLogs() - simError = fmt.Errorf("Simulation halted due to panic on block %d", header.Height) - } - }() - } - - for i := 0; i < numBlocks && !stopEarly; i++ { - // Log the header time for future lookup - pastTimes = append(pastTimes, header.Time) - pastVoteInfos = append(pastVoteInfos, request.LastCommitInfo.Votes) - - // Construct log writer - logWriter := addLogMessage(testingMode, blockLogBuilders, i) - - // Run the BeginBlock handler - logWriter("BeginBlock") - app.BeginBlock(request) - - if testingMode { - // Make sure invariants hold at beginning of block - assertAllInvariants(t, app, invariants, "BeginBlock", displayLogs) - } - - ctx := app.NewContext(false, header) - - // Run queued operations. Ignores blocksize if blocksize is too small - logWriter("Queued operations") - numQueuedOpsRan := runQueuedOperations(operationQueue, int(header.Height), tb, r, app, ctx, accs, logWriter, displayLogs, event) - numQueuedTimeOpsRan := runQueuedTimeOperations(timeOperationQueue, header.Time, tb, r, app, ctx, accs, logWriter, displayLogs, event) - if testingMode && onOperation { - // Make sure invariants hold at end of queued operations - assertAllInvariants(t, app, invariants, "QueuedOperations", displayLogs) - } - - logWriter("Standard operations") - operations := blockSimulator(r, app, ctx, accs, header, logWriter) - opCount += operations + numQueuedOpsRan + numQueuedTimeOpsRan - if testingMode { - // Make sure invariants hold at end of block - assertAllInvariants(t, app, invariants, "StandardOperations", displayLogs) - } - - res := app.EndBlock(abci.RequestEndBlock{}) - header.Height++ - header.Time = header.Time.Add(time.Duration(minTimePerBlock) * time.Second).Add(time.Duration(int64(r.Intn(int(timeDiff)))) * time.Second) - header.ProposerAddress = randomProposer(r, validators) - logWriter("EndBlock") - - if testingMode { - // Make sure invariants hold at end of block - assertAllInvariants(t, app, invariants, "EndBlock", displayLogs) - } - if commit { - app.Commit() - } - - if header.ProposerAddress == nil { - fmt.Printf("\nSimulation stopped early as all validators have been unbonded, there is nobody left propose a block!\n") - stopEarly = true - break - } - - // Generate a random RequestBeginBlock with the current validator set for the next block - request = RandomRequestBeginBlock(r, params, validators, pastTimes, pastVoteInfos, event, header) - - // Update the validator set, which will be reflected in the application on the next block - validators = nextValidators - nextValidators = updateValidators(tb, r, params, validators, res.ValidatorUpdates, event) - } - if stopEarly { - DisplayEvents(events) - return - } - fmt.Printf("\nSimulation complete. Final height (blocks): %d, final time (seconds), : %v, operations ran %d\n", header.Height, header.Time, opCount) - DisplayEvents(events) - return nil -} - -type blockSimFn func( - r *rand.Rand, app *bam.BaseApp, ctx sdk.Context, - accounts []Account, header abci.Header, logWriter func(string), -) (opCount int) - -// Returns a function to simulate blocks. Written like this to avoid constant parameters being passed everytime, to minimize -// memory overhead -func createBlockSimulator(testingMode bool, tb testing.TB, t *testing.T, params Params, - event func(string), invariants []Invariant, - ops []WeightedOperation, operationQueue map[int][]Operation, timeOperationQueue []FutureOperation, - totalNumBlocks int, avgBlockSize int, displayLogs func()) blockSimFn { - - var ( - lastBlocksizeState = 0 // state for [4 * uniform distribution] - totalOpWeight = 0 - blocksize int - ) - - for i := 0; i < len(ops); i++ { - totalOpWeight += ops[i].Weight - } - selectOp := func(r *rand.Rand) Operation { - x := r.Intn(totalOpWeight) - for i := 0; i < len(ops); i++ { - if x <= ops[i].Weight { - return ops[i].Op - } - x -= ops[i].Weight - } - // shouldn't happen - return ops[0].Op - } - - return func(r *rand.Rand, app *bam.BaseApp, ctx sdk.Context, - accounts []Account, header abci.Header, logWriter func(string)) (opCount int) { - fmt.Printf("\rSimulating... block %d/%d, operation %d/%d. ", header.Height, totalNumBlocks, opCount, blocksize) - lastBlocksizeState, blocksize = getBlockSize(r, params, lastBlocksizeState, avgBlockSize) - for j := 0; j < blocksize; j++ { - logUpdate, futureOps, err := selectOp(r)(r, app, ctx, accounts, event) - logWriter(logUpdate) - if err != nil { - displayLogs() - tb.Fatalf("error on operation %d within block %d, %v", header.Height, opCount, err) - } - - queueOperations(operationQueue, timeOperationQueue, futureOps) - if testingMode { - if onOperation { - assertAllInvariants(t, app, invariants, fmt.Sprintf("operation: %v", logUpdate), displayLogs) - } - if opCount%50 == 0 { - fmt.Printf("\rSimulating... block %d/%d, operation %d/%d. ", header.Height, totalNumBlocks, opCount, blocksize) - } - } - opCount++ - } - return opCount - } -} - -func getTestingMode(tb testing.TB) (testingMode bool, t *testing.T, b *testing.B) { - testingMode = false - if _t, ok := tb.(*testing.T); ok { - t = _t - testingMode = true - } else { - b = tb.(*testing.B) - } - return -} - -// getBlockSize returns a block size as determined from the transition matrix. -// It targets making average block size the provided parameter. The three -// states it moves between are: -// "over stuffed" blocks with average size of 2 * avgblocksize, -// normal sized blocks, hitting avgBlocksize on average, -// and empty blocks, with no txs / only txs scheduled from the past. -func getBlockSize(r *rand.Rand, params Params, lastBlockSizeState, avgBlockSize int) (state, blocksize int) { - // TODO: Make default blocksize transition matrix actually make the average - // blocksize equal to avgBlockSize. - state = params.BlockSizeTransitionMatrix.NextState(r, lastBlockSizeState) - if state == 0 { - blocksize = r.Intn(avgBlockSize * 4) - } else if state == 1 { - blocksize = r.Intn(avgBlockSize * 2) - } else { - blocksize = 0 - } - return -} - -// adds all future operations into the operation queue. -func queueOperations(queuedOperations map[int][]Operation, queuedTimeOperations []FutureOperation, futureOperations []FutureOperation) { - if futureOperations == nil { - return - } - for _, futureOp := range futureOperations { - if futureOp.BlockHeight != 0 { - if val, ok := queuedOperations[futureOp.BlockHeight]; ok { - queuedOperations[futureOp.BlockHeight] = append(val, futureOp.Op) - } else { - queuedOperations[futureOp.BlockHeight] = []Operation{futureOp.Op} - } - } else { - // TODO: Replace with proper sorted data structure, so don't have the copy entire slice - index := sort.Search(len(queuedTimeOperations), func(i int) bool { return queuedTimeOperations[i].BlockTime.After(futureOp.BlockTime) }) - queuedTimeOperations = append(queuedTimeOperations, FutureOperation{}) - copy(queuedTimeOperations[index+1:], queuedTimeOperations[index:]) - queuedTimeOperations[index] = futureOp - } - } -} - -// nolint: errcheck -func runQueuedOperations(queueOperations map[int][]Operation, height int, tb testing.TB, r *rand.Rand, app *bam.BaseApp, ctx sdk.Context, - accounts []Account, logWriter func(string), displayLogs func(), event func(string)) (numOpsRan int) { - if queuedOps, ok := queueOperations[height]; ok { - numOps := len(queuedOps) - for i := 0; i < numOps; i++ { - // For now, queued operations cannot queue more operations. - // If a need arises for us to support queued messages to queue more messages, this can - // be changed. - logUpdate, _, err := queuedOps[i](r, app, ctx, accounts, event) - logWriter(logUpdate) - if err != nil { - displayLogs() - tb.FailNow() - } - } - delete(queueOperations, height) - return numOps - } - return 0 -} - -func runQueuedTimeOperations(queueOperations []FutureOperation, currentTime time.Time, tb testing.TB, r *rand.Rand, app *bam.BaseApp, ctx sdk.Context, - accounts []Account, logWriter func(string), displayLogs func(), event func(string)) (numOpsRan int) { - - numOpsRan = 0 - for len(queueOperations) > 0 && currentTime.After(queueOperations[0].BlockTime) { - // For now, queued operations cannot queue more operations. - // If a need arises for us to support queued messages to queue more messages, this can - // be changed. - logUpdate, _, err := queueOperations[0].Op(r, app, ctx, accounts, event) - logWriter(logUpdate) - if err != nil { - displayLogs() - tb.FailNow() - } - queueOperations = queueOperations[1:] - numOpsRan++ - } - return numOpsRan -} - -func getKeys(validators map[string]mockValidator) []string { - keys := make([]string, len(validators)) - i := 0 - for key := range validators { - keys[i] = key - i++ - } - sort.Strings(keys) - return keys -} - -// randomProposer picks a random proposer from the current validator set -func randomProposer(r *rand.Rand, validators map[string]mockValidator) cmn.HexBytes { - keys := getKeys(validators) - if len(keys) == 0 { - return nil - } - key := keys[r.Intn(len(keys))] - proposer := validators[key].val - pk, err := tmtypes.PB2TM.PubKey(proposer.PubKey) - if err != nil { - panic(err) - } - return pk.Address() -} - -// RandomRequestBeginBlock generates a list of signing validators according to the provided list of validators, signing fraction, and evidence fraction -// nolint: unparam -func RandomRequestBeginBlock(r *rand.Rand, params Params, validators map[string]mockValidator, - pastTimes []time.Time, pastVoteInfos [][]abci.VoteInfo, event func(string), header abci.Header) abci.RequestBeginBlock { - if len(validators) == 0 { - fmt.Printf("validator is nil\n") - return abci.RequestBeginBlock{Header: header} - } - voteInfos := make([]abci.VoteInfo, len(validators)) - i := 0 - for _, key := range getKeys(validators) { - mVal := validators[key] - mVal.livenessState = params.LivenessTransitionMatrix.NextState(r, mVal.livenessState) - signed := true - - if mVal.livenessState == 1 { - // spotty connection, 50% probability of success - // See https://github.com/golang/go/issues/23804#issuecomment-365370418 - // for reasoning behind computing like this - signed = r.Int63()%2 == 0 - } else if mVal.livenessState == 2 { - // offline - signed = false - } - if signed { - event("beginblock/signing/signed") - } else { - event("beginblock/signing/missed") - } - pubkey, err := tmtypes.PB2TM.PubKey(mVal.val.PubKey) - if err != nil { - panic(err) - } - voteInfos[i] = abci.VoteInfo{ - Validator: abci.Validator{ - Address: pubkey.Address(), - Power: mVal.val.Power, - }, - SignedLastBlock: signed, - } - i++ - } - // TODO: Determine capacity before allocation - evidence := make([]abci.Evidence, 0) - // Anything but the first block - if len(pastTimes) > 0 { - for r.Float64() < params.EvidenceFraction { - height := header.Height - time := header.Time - vals := voteInfos - if r.Float64() < params.PastEvidenceFraction { - height = int64(r.Intn(int(header.Height) - 1)) - time = pastTimes[height] - vals = pastVoteInfos[height] - } - validator := vals[r.Intn(len(vals))].Validator - var totalVotingPower int64 - for _, val := range vals { - totalVotingPower += val.Validator.Power - } - evidence = append(evidence, abci.Evidence{ - Type: tmtypes.ABCIEvidenceTypeDuplicateVote, - Validator: validator, - Height: height, - Time: time, - TotalVotingPower: totalVotingPower, - }) - event("beginblock/evidence") - } - } - return abci.RequestBeginBlock{ - Header: header, - LastCommitInfo: abci.LastCommitInfo{ - Votes: voteInfos, - }, - ByzantineValidators: evidence, - } -} - -// updateValidators mimicks Tendermint's update logic -// nolint: unparam -func updateValidators(tb testing.TB, r *rand.Rand, params Params, current map[string]mockValidator, updates []abci.ValidatorUpdate, event func(string)) map[string]mockValidator { - - for _, update := range updates { - str := fmt.Sprintf("%v", update.PubKey) - switch { - case update.Power == 0: - if _, ok := current[str]; !ok { - tb.Fatalf("tried to delete a nonexistent validator") - } - - event("endblock/validatorupdates/kicked") - delete(current, str) - default: - // Does validator already exist? - if mVal, ok := current[str]; ok { - mVal.val = update - event("endblock/validatorupdates/updated") - } else { - // Set this new validator - current[str] = mockValidator{update, GetMemberOfInitialState(r, params.InitialLivenessWeightings)} - event("endblock/validatorupdates/added") - } - } - } - - return current -} diff --git a/modules/mock/simulation/simulate.go b/modules/mock/simulation/simulate.go new file mode 100644 index 000000000..065aef991 --- /dev/null +++ b/modules/mock/simulation/simulate.go @@ -0,0 +1,338 @@ +package simulation + +import ( + "encoding/json" + "fmt" + "math/rand" + "os" + "os/signal" + "runtime/debug" + "strings" + "syscall" + "testing" + "time" + + "github.com/irisnet/irishub/baseapp" + sdk "github.com/irisnet/irishub/types" + abci "github.com/tendermint/tendermint/abci/types" +) + +// AppStateFn returns the app state json bytes +type AppStateFn func(r *rand.Rand, accs []Account) json.RawMessage + +// Simulate tests application by sending random messages. +func Simulate(t *testing.T, app *baseapp.BaseApp, + appStateFn AppStateFn, ops WeightedOperations, + invariants Invariants, numBlocks int, blockSize int, commit bool) (bool, error) { + + time := time.Now().UnixNano() + return SimulateFromSeed(t, app, appStateFn, time, ops, + invariants, numBlocks, blockSize, commit) +} + +// initialize the chain for the simulation +func initChain(r *rand.Rand, params Params, accounts []Account, + app *baseapp.BaseApp, + appStateFn AppStateFn) mockValidators { + + req := abci.RequestInitChain{ + AppStateBytes: appStateFn(r, accounts), + } + res := app.InitChain(req) + validators := newMockValidators(r, res.Validators, params) + + return validators +} + +// SimulateFromSeed tests an application by running the provided +// operations, testing the provided invariants, but using the provided seed. +// TODO split this monster function up +func SimulateFromSeed(tb testing.TB, app *baseapp.BaseApp, + appStateFn AppStateFn, seed int64, ops WeightedOperations, + invariants Invariants, + numBlocks int, blockSize int, commit bool) (stopEarly bool, simError error) { + + // in case we have to end early, don't os.Exit so that we can run cleanup code. + testingMode, t, b := getTestingMode(tb) + fmt.Printf("Starting SimulateFromSeed with randomness "+ + "created with seed %d\n", int(seed)) + + r := rand.New(rand.NewSource(seed)) + params := RandomParams(r) // := DefaultParams() + fmt.Printf("Randomized simulation params: %+v\n", params) + + timestamp := RandTimestamp(r) + fmt.Printf("Starting the simulation from time %v, unixtime %v\n", + timestamp.UTC().Format(time.UnixDate), timestamp.Unix()) + + timeDiff := maxTimePerBlock - minTimePerBlock + accs := RandomAccounts(r, params.NumKeys) + eventStats := newEventStats() + + // Second variable to keep pending validator set (delayed one block since + // TM 0.24) Initially this is the same as the initial validator set + validators := initChain(r, params, accs, app, appStateFn) + nextValidators := validators + + header := abci.Header{ + Height: 1, + Time: timestamp, + ProposerAddress: validators.randomProposer(r), + } + opCount := 0 + + // Setup code to catch SIGTERM's + c := make(chan os.Signal) + signal.Notify(c, os.Interrupt, syscall.SIGTERM, syscall.SIGINT) + go func() { + receivedSignal := <-c + fmt.Printf("\nExiting early due to %s, on block %d, operation %d\n", + receivedSignal, header.Height, opCount) + simError = fmt.Errorf("Exited due to %s", receivedSignal) + stopEarly = true + }() + + var pastTimes []time.Time + var pastVoteInfos [][]abci.VoteInfo + + request := RandomRequestBeginBlock(r, params, + validators, pastTimes, pastVoteInfos, eventStats.tally, header) + + // These are operations which have been queued by previous operations + operationQueue := newOperationQueue() + timeOperationQueue := []FutureOperation{} + var blockLogBuilders []*strings.Builder + + if testingMode { + blockLogBuilders = make([]*strings.Builder, numBlocks) + } + displayLogs := logPrinter(testingMode, blockLogBuilders) + blockSimulator := createBlockSimulator( + testingMode, tb, t, params, eventStats.tally, invariants, + ops, operationQueue, timeOperationQueue, + numBlocks, blockSize, displayLogs) + + if !testingMode { + b.ResetTimer() + } else { + // Recover logs in case of panic + defer func() { + if r := recover(); r != nil { + fmt.Println("Panic with err\n", r) + stackTrace := string(debug.Stack()) + fmt.Println(stackTrace) + displayLogs() + simError = fmt.Errorf( + "Simulation halted due to panic on block %d", + header.Height) + } + }() + } + + // TODO split up the contents of this for loop into new functions + for i := 0; i < numBlocks && !stopEarly; i++ { + + // Log the header time for future lookup + pastTimes = append(pastTimes, header.Time) + pastVoteInfos = append(pastVoteInfos, request.LastCommitInfo.Votes) + + // Construct log writer + logWriter := addLogMessage(testingMode, blockLogBuilders, i) + + // Run the BeginBlock handler + logWriter("BeginBlock") + app.BeginBlock(request) + + if testingMode { + invariants.assertAll(t, app, "BeginBlock", displayLogs) + } + + ctx := app.NewContext(false, header) + + // Run queued operations. Ignores blocksize if blocksize is too small + logWriter("Queued operations") + numQueuedOpsRan := runQueuedOperations( + operationQueue, int(header.Height), + tb, r, app, ctx, accs, logWriter, + displayLogs, eventStats.tally) + + numQueuedTimeOpsRan := runQueuedTimeOperations( + timeOperationQueue, header.Time, + tb, r, app, ctx, accs, + logWriter, displayLogs, eventStats.tally) + + if testingMode && onOperation { + invariants.assertAll(t, app, "QueuedOperations", displayLogs) + } + + logWriter("Standard operations") + operations := blockSimulator(r, app, ctx, accs, header, logWriter) + opCount += operations + numQueuedOpsRan + numQueuedTimeOpsRan + if testingMode { + invariants.assertAll(t, app, "StandardOperations", displayLogs) + } + + res := app.EndBlock(abci.RequestEndBlock{}) + header.Height++ + header.Time = header.Time.Add( + time.Duration(minTimePerBlock) * time.Second) + header.Time = header.Time.Add( + time.Duration(int64(r.Intn(int(timeDiff)))) * time.Second) + header.ProposerAddress = validators.randomProposer(r) + logWriter("EndBlock") + + if testingMode { + invariants.assertAll(t, app, "EndBlock", displayLogs) + } + if commit { + app.Commit() + } + + if header.ProposerAddress == nil { + fmt.Printf("\nSimulation stopped early as all validators " + + "have been unbonded, there is nobody left propose a block!\n") + stopEarly = true + break + } + + // Generate a random RequestBeginBlock with the current validator set + // for the next block + request = RandomRequestBeginBlock(r, params, validators, + pastTimes, pastVoteInfos, eventStats.tally, header) + + // Update the validator set, which will be reflected in the application + // on the next block + validators = nextValidators + nextValidators = updateValidators(tb, r, params, + validators, res.ValidatorUpdates, eventStats.tally) + } + + if stopEarly { + eventStats.Print() + return true, simError + } + fmt.Printf("\nSimulation complete. Final height (blocks): %d, "+ + "final time (seconds), : %v, operations ran %d\n", + header.Height, header.Time, opCount) + + eventStats.Print() + return false, nil +} + +//______________________________________________________________________________ + +type blockSimFn func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, + accounts []Account, header abci.Header, logWriter func(string)) (opCount int) + +// Returns a function to simulate blocks. Written like this to avoid constant +// parameters being passed everytime, to minimize memory overhead. +func createBlockSimulator(testingMode bool, tb testing.TB, t *testing.T, params Params, + event func(string), invariants Invariants, ops WeightedOperations, + operationQueue OperationQueue, timeOperationQueue []FutureOperation, + totalNumBlocks int, avgBlockSize int, displayLogs func()) blockSimFn { + + var lastBlocksizeState = 0 // state for [4 * uniform distribution] + var blocksize int + selectOp := ops.getSelectOpFn() + + return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, + accounts []Account, header abci.Header, logWriter func(string)) (opCount int) { + + fmt.Printf("\rSimulating... block %d/%d, operation %d/%d. ", + header.Height, totalNumBlocks, opCount, blocksize) + lastBlocksizeState, blocksize = getBlockSize(r, params, lastBlocksizeState, avgBlockSize) + + type opAndR struct { + op Operation + rand *rand.Rand + } + opAndRz := make([]opAndR, 0, blocksize) + // Predetermine the blocksize slice so that we can do things like block + // out certain operations without changing the ops that follow. + for i := 0; i < blocksize; i++ { + opAndRz = append(opAndRz, opAndR{ + op: selectOp(r), + rand: DeriveRand(r), + }) + } + + for i := 0; i < blocksize; i++ { + // NOTE: the Rand 'r' should not be used here. + opAndR := opAndRz[i] + op, r2 := opAndR.op, opAndR.rand + logUpdate, futureOps, err := op(r2, app, ctx, accounts, event) + logWriter(logUpdate) + if err != nil { + displayLogs() + tb.Fatalf("error on operation %d within block %d, %v", + header.Height, opCount, err) + } + + queueOperations(operationQueue, timeOperationQueue, futureOps) + if testingMode { + if onOperation { + eventStr := fmt.Sprintf("operation: %v", logUpdate) + invariants.assertAll(t, app, eventStr, displayLogs) + } + if opCount%50 == 0 { + fmt.Printf("\rSimulating... block %d/%d, operation %d/%d. ", + header.Height, totalNumBlocks, opCount, blocksize) + } + } + opCount++ + } + return opCount + } +} + +// nolint: errcheck +func runQueuedOperations(queueOps map[int][]Operation, + height int, tb testing.TB, r *rand.Rand, app *baseapp.BaseApp, + ctx sdk.Context, accounts []Account, logWriter func(string), + displayLogs func(), tallyEvent func(string)) (numOpsRan int) { + + queuedOp, ok := queueOps[height] + if !ok { + return 0 + } + + numOpsRan = len(queuedOp) + for i := 0; i < numOpsRan; i++ { + + // For now, queued operations cannot queue more operations. + // If a need arises for us to support queued messages to queue more messages, this can + // be changed. + logUpdate, _, err := queuedOp[i](r, app, ctx, accounts, tallyEvent) + logWriter(logUpdate) + if err != nil { + displayLogs() + tb.FailNow() + } + } + delete(queueOps, height) + return numOpsRan +} + +func runQueuedTimeOperations(queueOps []FutureOperation, + currentTime time.Time, tb testing.TB, r *rand.Rand, + app *baseapp.BaseApp, ctx sdk.Context, accounts []Account, + logWriter func(string), displayLogs func(), tallyEvent func(string)) (numOpsRan int) { + + numOpsRan = 0 + for len(queueOps) > 0 && currentTime.After(queueOps[0].BlockTime) { + + // For now, queued operations cannot queue more operations. + // If a need arises for us to support queued messages to queue more messages, this can + // be changed. + logUpdate, _, err := queueOps[0].Op(r, app, ctx, accounts, tallyEvent) + logWriter(logUpdate) + if err != nil { + displayLogs() + tb.FailNow() + } + + queueOps = queueOps[1:] + numOpsRan++ + } + return numOpsRan +} diff --git a/modules/mock/simulation/transition_matrix.go b/modules/mock/simulation/transition_matrix.go index 39bdb1e4f..f7d713775 100644 --- a/modules/mock/simulation/transition_matrix.go +++ b/modules/mock/simulation/transition_matrix.go @@ -5,12 +5,11 @@ import ( "math/rand" ) -// TransitionMatrix is _almost_ a left stochastic matrix. -// It is technically not one due to not normalizing the column values. -// In the future, if we want to find the steady state distribution, -// it will be quite easy to normalize these values to get a stochastic matrix. -// Floats aren't currently used as the default due to non-determinism across -// architectures +// TransitionMatrix is _almost_ a left stochastic matrix. It is technically +// not one due to not normalizing the column values. In the future, if we want +// to find the steady state distribution, it will be quite easy to normalize +// these values to get a stochastic matrix. Floats aren't currently used as +// the default due to non-determinism across architectures type TransitionMatrix struct { weights [][]int // total in each column @@ -24,7 +23,8 @@ func CreateTransitionMatrix(weights [][]int) (TransitionMatrix, error) { n := len(weights) for i := 0; i < n; i++ { if len(weights[i]) != n { - return TransitionMatrix{}, fmt.Errorf("Transition Matrix: Non-square matrix provided, error on row %d", i) + return TransitionMatrix{}, + fmt.Errorf("Transition Matrix: Non-square matrix provided, error on row %d", i) } } totals := make([]int, n) @@ -36,8 +36,8 @@ func CreateTransitionMatrix(weights [][]int) (TransitionMatrix, error) { return TransitionMatrix{weights, totals, n}, nil } -// NextState returns the next state randomly chosen using r, and the weightings provided -// in the transition matrix. +// NextState returns the next state randomly chosen using r, and the weightings +// provided in the transition matrix. func (t TransitionMatrix) NextState(r *rand.Rand, i int) int { randNum := r.Intn(t.totals[i]) for row := 0; row < t.n; row++ { diff --git a/modules/mock/simulation/types.go b/modules/mock/simulation/types.go deleted file mode 100644 index 937919624..000000000 --- a/modules/mock/simulation/types.go +++ /dev/null @@ -1,87 +0,0 @@ -package simulation - -import ( - "math/rand" - "time" - - sdk "github.com/irisnet/irishub/types" - bam "github.com/irisnet/irishub/baseapp" - abci "github.com/tendermint/tendermint/abci/types" - "github.com/tendermint/tendermint/crypto" -) - -type ( - // Operation runs a state machine transition, - // and ensures the transition happened as expected. - // The operation could be running and testing a fuzzed transaction, - // or doing the same for a message. - // - // For ease of debugging, - // an operation returns a descriptive message "action", - // which details what this fuzzed state machine transition actually did. - // - // Operations can optionally provide a list of "FutureOperations" to run later - // These will be ran at the beginning of the corresponding block. - Operation func(r *rand.Rand, app *bam.BaseApp, ctx sdk.Context, - accounts []Account, event func(string), - ) (action string, futureOperations []FutureOperation, err error) - - // RandSetup performs the random setup the mock module needs. - RandSetup func(r *rand.Rand, accounts []Account) - - // An Invariant is a function which tests a particular invariant. - // If the invariant has been broken, it should return an error - // containing a descriptive message about what happened. - // The simulator will then halt and print the logs. - Invariant func(app *bam.BaseApp) error - - // Account contains a privkey, pubkey, address tuple - // eventually more useful data can be placed in here. - // (e.g. number of coins) - Account struct { - PrivKey crypto.PrivKey - PubKey crypto.PubKey - Address sdk.AccAddress - } - - mockValidator struct { - val abci.ValidatorUpdate - livenessState int - } - - // FutureOperation is an operation which will be ran at the - // beginning of the provided BlockHeight. - // If both a BlockHeight and BlockTime are specified, it will use the BlockHeight. - // In the (likely) event that multiple operations are queued at the same - // block height, they will execute in a FIFO pattern. - FutureOperation struct { - BlockHeight int - BlockTime time.Time - Op Operation - } - - // WeightedOperation is an operation with associated weight. - // This is used to bias the selection operation within the simulator. - WeightedOperation struct { - Weight int - Op Operation - } -) - -// TODO remove? not being called anywhere -// PeriodicInvariant returns an Invariant function closure that asserts -// a given invariant if the mock application's last block modulo the given -// period is congruent to the given offset. -func PeriodicInvariant(invariant Invariant, period int, offset int) Invariant { - return func(app *bam.BaseApp) error { - if int(app.LastBlockHeight())%period == offset { - return invariant(app) - } - return nil - } -} - -// nolint -func (acc Account) Equals(acc2 Account) bool { - return acc.Address.Equals(acc2.Address) -} diff --git a/modules/mock/simulation/util.go b/modules/mock/simulation/util.go index 2876bf4ff..99add8469 100644 --- a/modules/mock/simulation/util.go +++ b/modules/mock/simulation/util.go @@ -4,168 +4,119 @@ import ( "fmt" "math/rand" "os" - "sort" "strings" "testing" "time" - "github.com/tendermint/tendermint/crypto/ed25519" - "github.com/tendermint/tendermint/crypto/secp256k1" - "math/big" - "github.com/irisnet/irishub/modules/mock" - bam "github.com/irisnet/irishub/baseapp" - sdk "github.com/irisnet/irishub/types" -) -const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" -const ( - letterIdxBits = 6 // 6 bits to represent a letter index - letterIdxMask = 1<= 0; { - if remain == 0 { - cache, remain = r.Int63(), letterIdxMax - } - if idx := int(cache & letterIdxMask); idx < len(letterBytes) { - b[i] = letterBytes[idx] - i-- - } - cache >>= letterIdxBits - remain-- +func getTestingMode(tb testing.TB) (testingMode bool, t *testing.T, b *testing.B) { + testingMode = false + if _t, ok := tb.(*testing.T); ok { + t = _t + testingMode = true + } else { + b = tb.(*testing.B) } - return string(b) + return } -// Pretty-print events as a table -func DisplayEvents(events map[string]uint) { - var keys []string - for key := range events { - keys = append(keys, key) - } - sort.Strings(keys) - fmt.Printf("Event statistics: \n") - for _, key := range keys { - fmt.Printf(" % 60s => %d\n", key, events[key]) - } -} +// Builds a function to add logs for this particular block +func addLogMessage(testingmode bool, + blockLogBuilders []*strings.Builder, height int) func(string) { -// RandomAcc pick a random account from an array -func RandomAcc(r *rand.Rand, accs []Account) Account { - return accs[r.Intn( - len(accs), - )] -} + if !testingmode { + return func(_ string) {} + } -// Generate a random amount -func RandomAmount(r *rand.Rand, max sdk.Int) sdk.Int { - //return sdk.NewInt(int64(r.Intn(int(max.Int64())))) - if max.IsInt64() { - return sdk.NewInt(int64(r.Intn(int(max.Int64())))) - } else { - return sdk.NewInt(int64(r.Intn(int(int64(9223372036854775807))))) + blockLogBuilders[height] = &strings.Builder{} + return func(x string) { + (*blockLogBuilders[height]).WriteString(x) + (*blockLogBuilders[height]).WriteString("\n") } } -// RandomDecAmount generates a random decimal amount -func RandomDecAmount(r *rand.Rand, max sdk.Dec) sdk.Dec { - randInt := big.NewInt(0).Rand(r, max.Int) - return sdk.NewDecFromBigIntWithPrec(randInt, sdk.Precision) -} +// Creates a function to print out the logs +func logPrinter(testingmode bool, logs []*strings.Builder) func() { + if !testingmode { + return func() {} + } -// RandomAccounts generates n random accounts -func RandomAccounts(r *rand.Rand, n int) []Account { - accs := make([]Account, n) - for i := 0; i < n; i++ { - // don't need that much entropy for simulation - privkeySeed := make([]byte, 15) - r.Read(privkeySeed) - useSecp := r.Int63()%2 == 0 - if useSecp { - accs[i].PrivKey = secp256k1.GenPrivKeySecp256k1(privkeySeed) - } else { - accs[i].PrivKey = ed25519.GenPrivKeyFromSecret(privkeySeed) + return func() { + numLoggers := 0 + for i := 0; i < len(logs); i++ { + // We're passed the last created block + if logs[i] == nil { + numLoggers = i + break + } } - accs[i].PubKey = accs[i].PrivKey.PubKey() - accs[i].Address = sdk.AccAddress(accs[i].PubKey.Address()) - } - return accs -} -// Builds a function to add logs for this particular block -func addLogMessage(testingmode bool, blockLogBuilders []*strings.Builder, height int) func(string) { - if testingmode { - blockLogBuilders[height] = &strings.Builder{} - return func(x string) { - (*blockLogBuilders[height]).WriteString(x) - (*blockLogBuilders[height]).WriteString("\n") + var f *os.File + if numLoggers > 10 { + fileName := fmt.Sprintf("simulation_log_%s.txt", + time.Now().Format("2006-01-02 15:04:05")) + fmt.Printf("Too many logs to display, instead writing to %s\n", + fileName) + f, _ = os.Create(fileName) } - } - return func(x string) {} -} -// assertAllInvariants asserts a list of provided invariants against application state -func assertAllInvariants(t *testing.T, app *bam.BaseApp, - invariants []Invariant, where string, displayLogs func()) { + for i := 0; i < numLoggers; i++ { + if f == nil { + fmt.Printf("Begin block %d\n", i+1) + fmt.Println((*logs[i]).String()) + continue + } + + _, err := f.WriteString(fmt.Sprintf("Begin block %d\n", i+1)) + if err != nil { + panic("Failed to write logs to file") + } - for i := 0; i < len(invariants); i++ { - err := invariants[i](app) - if err != nil { - fmt.Printf("Invariants broken after %s\n", where) - fmt.Println(err.Error()) - displayLogs() - t.Fatal() + _, err = f.WriteString((*logs[i]).String()) + if err != nil { + panic("Failed to write logs to file") + } } } } -// RandomSetGenesis wraps mock.RandomSetGenesis, but using simulation accounts -func RandomSetGenesis(r *rand.Rand, app *mock.App, accs []Account, denoms []string) { - addrs := make([]sdk.AccAddress, len(accs)) - for i := 0; i < len(accs); i++ { - addrs[i] = accs[i].Address +// getBlockSize returns a block size as determined from the transition matrix. +// It targets making average block size the provided parameter. The three +// states it moves between are: +// - "over stuffed" blocks with average size of 2 * avgblocksize, +// - normal sized blocks, hitting avgBlocksize on average, +// - and empty blocks, with no txs / only txs scheduled from the past. +func getBlockSize(r *rand.Rand, params Params, + lastBlockSizeState, avgBlockSize int) (state, blocksize int) { + + // TODO: Make default blocksize transition matrix actually make the average + // blocksize equal to avgBlockSize. + state = params.BlockSizeTransitionMatrix.NextState(r, lastBlockSizeState) + switch state { + case 0: + blocksize = r.Intn(avgBlockSize * 4) + case 1: + blocksize = r.Intn(avgBlockSize * 2) + default: + blocksize = 0 } - mock.RandomSetGenesis(r, app, addrs, denoms) + return state, blocksize } -// Creates a function to print out the logs -func logPrinter(testingmode bool, logs []*strings.Builder) func() { - if testingmode { - return func() { - numLoggers := 0 - for i := 0; i < len(logs); i++ { - // We're passed the last created block - if logs[i] == nil { - numLoggers = i - break - } - } - var f *os.File - if numLoggers > 10 { - fileName := fmt.Sprintf("simulation_log_%s.txt", time.Now().Format("2006-01-02 15:04:05")) - fmt.Printf("Too many logs to display, instead writing to %s\n", fileName) - f, _ = os.Create(fileName) - } - for i := 0; i < numLoggers; i++ { - if f != nil { - _, err := f.WriteString(fmt.Sprintf("Begin block %d\n", i+1)) - if err != nil { - panic("Failed to write logs to file") - } - _, err = f.WriteString((*logs[i]).String()) - if err != nil { - panic("Failed to write logs to file") - } - } else { - fmt.Printf("Begin block %d\n", i+1) - fmt.Println((*logs[i]).String()) - } - } +// PeriodicInvariant returns an Invariant function closure that asserts a given +// invariant if the mock application's last block modulo the given period is +// congruent to the given offset. +// +// NOTE this function is intended to be used manually used while running +// computationally heavy simulations. +// TODO reference this function in the codebase probably through use of a switch +func PeriodicInvariant(invariant Invariant, period int, offset int) Invariant { + return func(ctx sdk.Context) error { + if int(ctx.BlockHeight())%period == offset { + return invariant(ctx) } + return nil } - return func() {} } diff --git a/modules/slashing/genesis.go b/modules/slashing/genesis.go index 43a31f567..9de9816c1 100644 --- a/modules/slashing/genesis.go +++ b/modules/slashing/genesis.go @@ -41,7 +41,7 @@ func InitGenesis(ctx sdk.Context, keeper Keeper, data GenesisState, sdata types. if err != nil { panic(err) } - keeper.setValidatorSigningInfo(ctx, address, info) + keeper.SetValidatorSigningInfo(ctx, address, info) } for addr, array := range data.MissedBlocks { @@ -70,7 +70,7 @@ func ExportGenesis(ctx sdk.Context, keeper Keeper) (data GenesisState) { signingInfos := make(map[string]ValidatorSigningInfo) missedBlocks := make(map[string][]MissedBlock) - keeper.iterateValidatorSigningInfos(ctx, func(address sdk.ConsAddress, info ValidatorSigningInfo) (stop bool) { + keeper.IterateValidatorSigningInfos(ctx, func(address sdk.ConsAddress, info ValidatorSigningInfo) (stop bool) { bechAddr := address.String() signingInfos[bechAddr] = info localMissedBlocks := []MissedBlock{} diff --git a/modules/slashing/hooks.go b/modules/slashing/hooks.go index 395b285ed..fba98fdca 100644 --- a/modules/slashing/hooks.go +++ b/modules/slashing/hooks.go @@ -18,7 +18,7 @@ func (k Keeper) onValidatorBonded(ctx sdk.Context, address sdk.ConsAddress, _ sd JailedUntil: time.Unix(0, 0), MissedBlocksCounter: 0, } - k.setValidatorSigningInfo(ctx, address, signingInfo) + k.SetValidatorSigningInfo(ctx, address, signingInfo) } // Create a new slashing period when a validator is bonded diff --git a/modules/slashing/keeper.go b/modules/slashing/keeper.go index 77e7ac762..4ec03e238 100644 --- a/modules/slashing/keeper.go +++ b/modules/slashing/keeper.go @@ -96,7 +96,7 @@ func (k Keeper) handleDoubleSign(ctx sdk.Context, addr crypto.Address, infractio panic(fmt.Sprintf("Expected signing info for validator %s but not found", consAddr)) } signInfo.JailedUntil = time.Add(k.DoubleSignUnbondDuration(ctx)) - k.setValidatorSigningInfo(ctx, consAddr, signInfo) + k.SetValidatorSigningInfo(ctx, consAddr, signInfo) return } @@ -170,7 +170,7 @@ func (k Keeper) handleValidatorSignature(ctx sdk.Context, addr crypto.Address, p } // Set the updated signing info - k.setValidatorSigningInfo(ctx, consAddr, signInfo) + k.SetValidatorSigningInfo(ctx, consAddr, signInfo) return } diff --git a/modules/slashing/signing_info.go b/modules/slashing/signing_info.go index 60e0af151..981f32bbd 100644 --- a/modules/slashing/signing_info.go +++ b/modules/slashing/signing_info.go @@ -21,7 +21,7 @@ func (k Keeper) getValidatorSigningInfo(ctx sdk.Context, address sdk.ConsAddress } // Stored by *validator* address (not operator address) -func (k Keeper) iterateValidatorSigningInfos(ctx sdk.Context, handler func(address sdk.ConsAddress, info ValidatorSigningInfo) (stop bool)) { +func (k Keeper) IterateValidatorSigningInfos(ctx sdk.Context, handler func(address sdk.ConsAddress, info ValidatorSigningInfo) (stop bool)) { store := ctx.KVStore(k.storeKey) iter := sdk.KVStorePrefixIterator(store, ValidatorSigningInfoKey) defer iter.Close() @@ -36,7 +36,7 @@ func (k Keeper) iterateValidatorSigningInfos(ctx sdk.Context, handler func(addre } // Stored by *validator* address (not operator address) -func (k Keeper) setValidatorSigningInfo(ctx sdk.Context, address sdk.ConsAddress, info ValidatorSigningInfo) { +func (k Keeper) SetValidatorSigningInfo(ctx sdk.Context, address sdk.ConsAddress, info ValidatorSigningInfo) { store := ctx.KVStore(k.storeKey) bz := k.cdc.MustMarshalBinaryLengthPrefixed(info) store.Set(GetValidatorSigningInfoKey(address), bz) diff --git a/modules/slashing/simulation/invariants.go b/modules/slashing/simulation/invariants.go index 3cb6d57d8..4cbd8b991 100644 --- a/modules/slashing/simulation/invariants.go +++ b/modules/slashing/simulation/invariants.go @@ -1,14 +1,14 @@ package simulation import ( - "github.com/irisnet/irishub/baseapp" "github.com/irisnet/irishub/modules/mock/simulation" + sdk "github.com/irisnet/irishub/types" ) // TODO Any invariants to check here? // AllInvariants tests all slashing invariants func AllInvariants() simulation.Invariant { - return func(_ *baseapp.BaseApp) error { + return func(_ sdk.Context) error { return nil } } diff --git a/modules/slashing/slashing_period.go b/modules/slashing/slashing_period.go index fce18aa0b..f95e52088 100644 --- a/modules/slashing/slashing_period.go +++ b/modules/slashing/slashing_period.go @@ -66,6 +66,16 @@ func (k Keeper) iterateValidatorSlashingPeriods(ctx sdk.Context, handler func(sl } } +// Delete all slashing periods in the store. +func (k Keeper) DeleteValidatorSlashingPeriods(ctx sdk.Context) { + store := ctx.KVStore(k.storeKey) + iter := sdk.KVStorePrefixIterator(store, ValidatorSlashingPeriodKey) + for ; iter.Valid(); iter.Next() { + store.Delete(iter.Key()) + } + iter.Close() +} + // Stored by validator Tendermint address (not operator address) // This function sets a validator slashing period for a particular validator, // start height, end height, and current slashed-so-far total, or updates diff --git a/modules/stake/genesis.go b/modules/stake/genesis.go index 793f39ae1..95af7755c 100644 --- a/modules/stake/genesis.go +++ b/modules/stake/genesis.go @@ -22,7 +22,7 @@ func InitGenesis(ctx sdk.Context, keeper Keeper, data types.GenesisState) (res [ // We need to pretend to be "n blocks before genesis", where "n" is the validator update delay, // so that e.g. slashing periods are correctly initialized for the validator set // e.g. with a one-block offset - the first TM block is at height 0, so state updates applied from genesis.json are in block -1. - ctx = ctx.WithBlockHeight(-types.ValidatorUpdateDelay) + ctx = ctx.WithBlockHeight(1 - types.ValidatorUpdateDelay) keeper.SetPool(ctx, data.Pool) keeper.SetParams(ctx, data.Params) diff --git a/modules/stake/simulation/invariants.go b/modules/stake/simulation/invariants.go index 290cf0123..e351a0bcc 100644 --- a/modules/stake/simulation/invariants.go +++ b/modules/stake/simulation/invariants.go @@ -3,36 +3,45 @@ package simulation import ( "bytes" "fmt" + sdk "github.com/irisnet/irishub/types" "github.com/irisnet/irishub/modules/auth" "github.com/irisnet/irishub/modules/bank" "github.com/irisnet/irishub/modules/distribution" + "github.com/irisnet/irishub/modules/mock/simulation" "github.com/irisnet/irishub/modules/stake" + "github.com/irisnet/irishub/modules/service" "github.com/irisnet/irishub/modules/stake/keeper" - "github.com/irisnet/irishub/baseapp" - "github.com/irisnet/irishub/modules/mock/simulation" - abci "github.com/tendermint/tendermint/abci/types" - "github.com/irisnet/irishub/modules/stake/types" + stakeTypes "github.com/irisnet/irishub/modules/stake/types" ) // AllInvariants runs all invariants of the stake module. // Currently: total supply, positive power func AllInvariants(ck bank.Keeper, k stake.Keeper, - f auth.FeeCollectionKeeper, d distribution.Keeper, + f auth.FeeCollectionKeeper, d distribution.Keeper, s service.Keeper, am auth.AccountKeeper) simulation.Invariant { - return func(app *baseapp.BaseApp) error { - //err := SupplyInvariants(ck, k, f, d, am)(app, header) - //if err != nil { - // return err - //} - //err = PositivePowerInvariant(k)(app, header) - //if err != nil { - // return err - //} - //err = ValidatorSetInvariant(k)(app, header) - //return err - //return nil + return func(ctx sdk.Context) error { + err := SupplyInvariants(ck, k, f, d, s, am)(ctx) + if err != nil { + return err + } + + err = PositivePowerInvariant(k)(ctx) + if err != nil { + return err + } + + err = PositiveDelegationInvariant(k)(ctx) + if err != nil { + return err + } + + err = DelegatorSharesInvariant(k)(ctx) + if err != nil { + return err + } + return nil } } @@ -40,21 +49,21 @@ func AllInvariants(ck bank.Keeper, k stake.Keeper, // SupplyInvariants checks that the total supply reflects all held loose tokens, bonded tokens, and unbonding delegations // nolint: unparam func SupplyInvariants(ck bank.Keeper, k stake.Keeper, - f auth.FeeCollectionKeeper, d distribution.Keeper, am auth.AccountKeeper) simulation.Invariant { - return func(app *baseapp.BaseApp) error { - ctx := app.NewContext(false, abci.Header{}) + f auth.FeeCollectionKeeper, d distribution.Keeper, s service.Keeper, am auth.AccountKeeper) simulation.Invariant { + return func(ctx sdk.Context) error { pool := k.GetPool(ctx) loose := sdk.ZeroDec() bonded := sdk.ZeroDec() am.IterateAccounts(ctx, func(acc auth.Account) bool { - loose = loose.Add(sdk.NewDecFromInt(acc.GetCoins().AmountOf(types.StakeDenom))) + loose = loose.Add(sdk.NewDecFromInt(acc.GetCoins().AmountOf(stakeTypes.StakeDenom))) return false }) k.IterateUnbondingDelegations(ctx, func(_ int64, ubd stake.UnbondingDelegation) bool { loose = loose.Add(sdk.NewDecFromInt(ubd.Balance.Amount)) return false }) + validatorCount := int64(0) k.IterateValidators(ctx, func(_ int64, validator sdk.Validator) bool { switch validator.GetStatus() { case sdk.Bonded: @@ -64,25 +73,30 @@ func SupplyInvariants(ck bank.Keeper, k stake.Keeper, case sdk.Unbonded: loose = loose.Add(validator.GetTokens()) } + validatorCount++ return false }) feePool := d.GetFeePool(ctx) // add outstanding fees - loose = loose.Add(sdk.NewDecFromInt(f.GetCollectedFees(ctx).AmountOf(types.StakeDenom))) + loose = loose.Add(sdk.NewDecFromInt(f.GetCollectedFees(ctx).AmountOf(stakeTypes.StakeDenom))) // add community pool - loose = loose.Add(feePool.CommunityPool.AmountOf(types.StakeDenom)) + loose = loose.Add(feePool.CommunityPool.AmountOf(stakeTypes.StakeDenom)) // add validator distribution pool - loose = loose.Add(feePool.ValPool.AmountOf(types.StakeDenom)) + loose = loose.Add(feePool.ValPool.AmountOf(stakeTypes.StakeDenom)) + + servicePool := s.GetServiceFeeTaxPool(ctx) + + loose = loose.Add(sdk.NewDecFromInt(servicePool.AmountOf(stakeTypes.StakeDenom))) // add validator distribution commission and yet-to-be-withdrawn-by-delegators d.IterateValidatorDistInfos(ctx, func(_ int64, distInfo distribution.ValidatorDistInfo) (stop bool) { - loose = loose.Add(distInfo.DelPool.AmountOf(types.StakeDenom)) - loose = loose.Add(distInfo.ValCommission.AmountOf(types.StakeDenom)) + loose = loose.Add(distInfo.DelPool.AmountOf(stakeTypes.StakeDenom)) + loose = loose.Add(distInfo.ValCommission.AmountOf(stakeTypes.StakeDenom)) return false }, ) @@ -94,21 +108,21 @@ func SupplyInvariants(ck bank.Keeper, k stake.Keeper, "\n\tsum of account tokens: %v", pool.LooseTokens, loose) } + equivalent := pool.BondedTokens.QuoInt(sdk.NewIntWithDecimal(1, 18)) + // Bonded tokens should equal sum of tokens with bonded validators - if !pool.BondedTokens.Equal(bonded) { + if !equivalent.GTE(bonded) || !bonded.GTE(equivalent.Sub(sdk.NewDec(validatorCount))) { return fmt.Errorf("bonded token invariance:\n\tpool.BondedTokens: %v"+ - "\n\tsum of account tokens: %v", pool.BondedTokens, bonded) + "\n\tsum of account tokens: %v", equivalent, bonded) } return nil } } -// PositivePowerInvariant checks that all stored validators have > 0 power +// PositivePowerInvariant checks that all stored validators have > 0 power. func PositivePowerInvariant(k stake.Keeper) simulation.Invariant { - return func(app *baseapp.BaseApp) error { - ctx := app.NewContext(false, abci.Header{}) - + return func(ctx sdk.Context) error { iterator := k.ValidatorsPowerStoreIterator(ctx) pool := k.GetPool(ctx) @@ -130,10 +144,45 @@ func PositivePowerInvariant(k stake.Keeper) simulation.Invariant { } } -// ValidatorSetInvariant checks equivalence of Tendermint validator set and SDK validator set -func ValidatorSetInvariant(k stake.Keeper) simulation.Invariant { - return func(app *baseapp.BaseApp) error { - // TODO +// PositiveDelegationInvariant checks that all stored delegations have > 0 shares. +func PositiveDelegationInvariant(k stake.Keeper) simulation.Invariant { + return func(ctx sdk.Context) error { + delegations := k.GetAllDelegations(ctx) + for _, delegation := range delegations { + if delegation.Shares.IsNegative() { + return fmt.Errorf("delegation with negative shares: %+v", delegation) + } + if delegation.Shares.IsZero() { + return fmt.Errorf("delegation with zero shares: %+v", delegation) + } + } + return nil } } + +// DelegatorSharesInvariant checks whether all the delegator shares which persist +// in the delegator object add up to the correct total delegator shares +// amount stored in each validator +func DelegatorSharesInvariant(k stake.Keeper) simulation.Invariant { + return func(ctx sdk.Context) error { + validators := k.GetAllValidators(ctx) + for _, validator := range validators { + + valTotalDelShares := validator.GetDelegatorShares() + + totalDelShares := sdk.ZeroDec() + delegations := k.GetValidatorDelegations(ctx, validator.GetOperator()) + for _, delegation := range delegations { + totalDelShares = totalDelShares.Add(delegation.Shares) + } + + if !valTotalDelShares.Equal(totalDelShares) { + return fmt.Errorf("broken delegator shares invariance:\n"+ + "\tvalidator.DelegatorShares: %v\n"+ + "\tsum of Delegator.Shares: %v", valTotalDelShares, totalDelShares) + } + } + return nil + } +} \ No newline at end of file diff --git a/modules/stake/simulation/msgs.go b/modules/stake/simulation/msgs.go index 8c04489d3..651602301 100644 --- a/modules/stake/simulation/msgs.go +++ b/modules/stake/simulation/msgs.go @@ -7,9 +7,7 @@ import ( "github.com/irisnet/irishub/modules/stake" "github.com/irisnet/irishub/modules/stake/keeper" "github.com/irisnet/irishub/baseapp" - "github.com/irisnet/irishub/modules/mock" "github.com/irisnet/irishub/modules/mock/simulation" - abci "github.com/tendermint/tendermint/abci/types" "math/rand" ) @@ -27,9 +25,9 @@ func SimulateMsgCreateValidator(m auth.AccountKeeper, k stake.Keeper) simulation maxCommission := sdk.NewInt(10) commission := stake.NewCommissionMsg( - sdk.NewDecWithPrec(simulation.RandomAmount(r, maxCommission).Int64(), 1), - sdk.NewDecWithPrec(simulation.RandomAmount(r, maxCommission).Int64(), 1), - sdk.NewDecWithPrec(simulation.RandomAmount(r, maxCommission).Int64(), 1), + sdk.NewDecWithPrec(maxCommission.Int64(), 1), + sdk.NewDecWithPrec(maxCommission.Int64(), 1), + sdk.NewDecWithPrec(maxCommission.Int64(), 1), ) acc := simulation.RandomAcc(r, accs) @@ -37,9 +35,6 @@ func SimulateMsgCreateValidator(m auth.AccountKeeper, k stake.Keeper) simulation amount := m.GetAccount(ctx, acc.Address).GetCoins().AmountOf(denom) - if amount.GT(sdk.ZeroInt()) { - amount = simulation.RandomAmount(r, amount) - } if amount.Equal(sdk.ZeroInt()) { return "no-operation", nil, nil @@ -87,7 +82,7 @@ func SimulateMsgEditValidator(k stake.Keeper) simulation.Operation { } maxCommission := sdk.NewInt(10) - newCommissionRate := sdk.NewDecWithPrec(simulation.RandomAmount(r, maxCommission).Int64(), 1) + newCommissionRate := sdk.NewDecWithPrec(maxCommission.Int64(), 1) val := keeper.RandomValidator(r, k, ctx) address := val.GetOperator() @@ -203,9 +198,6 @@ func SimulateMsgBeginRedelegate(m auth.AccountKeeper, k stake.Keeper) simulation delegatorAddress := delegatorAcc.Address // TODO amount := m.GetAccount(ctx, delegatorAddress).GetCoins().AmountOf(denom) - if amount.GT(sdk.ZeroInt()) { - amount = simulation.RandomAmount(r, amount) - } if amount.Equal(sdk.ZeroInt()) { return "no-operation", nil, nil } @@ -228,26 +220,3 @@ func SimulateMsgBeginRedelegate(m auth.AccountKeeper, k stake.Keeper) simulation return action, nil, nil } } - -// Setup -// nolint: errcheck -func Setup(mapp *mock.App, k stake.Keeper) simulation.RandSetup { - return func(r *rand.Rand, accs []simulation.Account) { - ctx := mapp.NewContext(false, abci.Header{}) - gen := stake.DefaultGenesisState() - stake.InitGenesis(ctx, k, gen) - params := k.GetParams(ctx) - denom := params.BondDenom - loose := sdk.ZeroInt() - mapp.AccountKeeper.IterateAccounts(ctx, func(acc auth.Account) bool { - balance := simulation.RandomAmount(r, sdk.NewInt(1000000)) - acc.SetCoins(acc.GetCoins().Plus(sdk.Coins{sdk.NewCoin(denom, balance)})) - mapp.AccountKeeper.SetAccount(ctx, acc) - loose = loose.Add(balance) - return false - }) - pool := k.GetPool(ctx) - pool.LooseTokens = pool.LooseTokens.Add(sdk.NewDec(loose.Int64())) - k.SetPool(ctx, pool) - } -} diff --git a/modules/stake/simulation/sim_test.go b/modules/stake/simulation/sim_test.go deleted file mode 100644 index 89bbd2259..000000000 --- a/modules/stake/simulation/sim_test.go +++ /dev/null @@ -1,106 +0,0 @@ -package simulation - -import ( - "encoding/json" - "math/rand" - "testing" - - sdk "github.com/irisnet/irishub/types" - "github.com/irisnet/irishub/modules/auth" - "github.com/irisnet/irishub/modules/bank" - "github.com/irisnet/irishub/modules/distribution" - "github.com/irisnet/irishub/modules/params" - "github.com/irisnet/irishub/modules/stake" - "github.com/irisnet/irishub/modules/mock" - "github.com/irisnet/irishub/modules/mock/simulation" - abci "github.com/tendermint/tendermint/abci/types" - "github.com/irisnet/irishub/modules/stake/types" -) - -// TestStakeWithRandomMessages -func TestStakeWithRandomMessages(t *testing.T) { - mapp := mock.NewApp() - - bank.RegisterCodec(mapp.Cdc) - stake.RegisterCodec(mapp.Cdc) - - mapper := mapp.AccountKeeper - bankKeeper := mapp.BankKeeper - - feeKey := mapp.KeyFeeCollection - stakeKey := mapp.KeyStake - stakeTKey := mapp.TkeyStake - paramsKey := mapp.KeyParams - paramsTKey := mapp.TkeyParams - distrKey := sdk.NewKVStoreKey("distr") - - feeCollectionKeeper := auth.NewFeeCollectionKeeper(mapp.Cdc, feeKey) - paramstore := params.NewKeeper(mapp.Cdc, paramsKey, paramsTKey) - stakeKeeper := stake.NewKeeper(mapp.Cdc, stakeKey, stakeTKey, bankKeeper, paramstore.Subspace(stake.DefaultParamspace), stake.DefaultCodespace) - distrKeeper := distribution.NewKeeper(mapp.Cdc, distrKey, paramstore.Subspace(distribution.DefaultParamspace), bankKeeper, stakeKeeper, feeCollectionKeeper, distribution.DefaultCodespace) - mapp.Router().AddRoute("stake", []*sdk.KVStoreKey{stakeKey, mapp.KeyAccount, distrKey}, stake.NewHandler(stakeKeeper)) - mapp.SetEndBlocker(func(ctx sdk.Context, req abci.RequestEndBlock) abci.ResponseEndBlock { - validatorUpdates := stake.EndBlocker(ctx, stakeKeeper) - return abci.ResponseEndBlock{ - ValidatorUpdates: validatorUpdates, - } - }) - - err := mapp.CompleteSetup(distrKey) - if err != nil { - panic(err) - } - - appStateFn := func(r *rand.Rand, accs []simulation.Account) json.RawMessage { - simulation.RandomSetGenesis(r, mapp, accs, []string{types.StakeDenom}) - return json.RawMessage("{}") - } - - GenesisSetUp := func(r *rand.Rand, accs []simulation.Account) { - ctx := mapp.NewContext(false, abci.Header{}) - distribution.InitGenesis(ctx, distrKeeper, distribution.DefaultGenesisState()) - - // init stake genesis - var ( - validators []stake.Validator - delegations []stake.Delegation - ) - stakeGenesis := stake.DefaultGenesisState() - - // XXX Try different numbers of initially bonded validators - numInitiallyBonded := int64(4) - valAddrs := make([]sdk.ValAddress, numInitiallyBonded) - decAmt := sdk.NewDecFromInt(sdk.NewIntWithDecimal(1, 2)) - for i := 0; i < int(numInitiallyBonded); i++ { - valAddr := sdk.ValAddress(accs[i].Address) - valAddrs[i] = valAddr - - validator := stake.NewValidator(valAddr, accs[i].PubKey, stake.Description{}) - validator.Tokens = decAmt - validator.DelegatorShares = decAmt - delegation := stake.Delegation{accs[i].Address, valAddr, decAmt, 0} - validators = append(validators, validator) - delegations = append(delegations, delegation) - } - stakeGenesis.Pool.LooseTokens = sdk.NewDecFromInt(sdk.NewIntWithDecimal(1, 10)) - stakeGenesis.Validators = validators - stakeGenesis.Bonds = delegations - - stake.InitGenesis(ctx, stakeKeeper, stakeGenesis) - } - - simulation.Simulate( - t, mapp.BaseApp, appStateFn, - []simulation.WeightedOperation{ - {10, SimulateMsgCreateValidator(mapper, stakeKeeper)}, - {5, SimulateMsgEditValidator(stakeKeeper)}, - {15, SimulateMsgDelegate(mapper, stakeKeeper)}, - {10, SimulateMsgBeginUnbonding(mapper, stakeKeeper)}, - {10, SimulateMsgBeginRedelegate(mapper, stakeKeeper)}, - }, []simulation.RandSetup{ - //Setup(mapp, stakeKeeper), - GenesisSetUp, - }, []simulation.Invariant{}, 10, 100, - false, - ) -} diff --git a/server/constructors.go b/server/constructors.go index 9039d8a81..909bda8e2 100644 --- a/server/constructors.go +++ b/server/constructors.go @@ -19,7 +19,7 @@ type ( // AppExporter is a function that dumps all app state to // JSON-serializable structure and returns the current validator set. - AppExporter func(log.Logger, dbm.DB, io.Writer, int64) (json.RawMessage, []tmtypes.GenesisValidator, error) + AppExporter func(log.Logger, dbm.DB, io.Writer, int64, bool) (json.RawMessage, []tmtypes.GenesisValidator, error) ) func openDB(rootDir string) (dbm.DB, error) { diff --git a/server/export.go b/server/export.go index f7a4dd28a..b2274abf7 100644 --- a/server/export.go +++ b/server/export.go @@ -15,6 +15,7 @@ import ( const ( flagHeight = "height" + flagForZeroHeight = "for-zero-height" ) // ExportCmd dumps app state to JSON. @@ -50,7 +51,8 @@ func ExportCmd(ctx *Context, cdc *codec.Codec, appExporter AppExporter) *cobra.C return err } height := viper.GetInt64(flagHeight) - appState, validators, err := appExporter(ctx.Logger, db, traceWriter, height) + forZeroHeight := viper.GetBool(flagForZeroHeight) + appState, validators, err := appExporter(ctx.Logger, db, traceWriter, height, forZeroHeight) if err != nil { return errors.Errorf("error exporting state: %v\n", err) } @@ -73,6 +75,7 @@ func ExportCmd(ctx *Context, cdc *codec.Codec, appExporter AppExporter) *cobra.C }, } cmd.Flags().Int64(flagHeight, -1, "Export state from a particular height (-1 means latest height)") + cmd.Flags().Bool(flagForZeroHeight, false, "Export state to start at height zero (perform preproccessing)") return cmd } diff --git a/types/decimal.go b/types/decimal.go index 3c0e89f60..275747b64 100644 --- a/types/decimal.go +++ b/types/decimal.go @@ -176,6 +176,8 @@ func NewDecFromStr(str string) (d Dec, err Error) { //nolint func (d Dec) IsNil() bool { return d.Int == nil } // is decimal nil func (d Dec) IsZero() bool { return (d.Int).Sign() == 0 } // is equal to zero +func (d Dec) IsNegative() bool { return (d.Int).Sign() == -1 } // is negative +func (d Dec) IsPositive() bool { return (d.Int).Sign() == 1 } // is positive func (d Dec) Equal(d2 Dec) bool { return (d.Int).Cmp(d2.Int) == 0 } // equal decimals func (d Dec) GT(d2 Dec) bool { return (d.Int).Cmp(d2.Int) > 0 } // greater than func (d Dec) GTE(d2 Dec) bool { return (d.Int).Cmp(d2.Int) >= 0 } // greater than or equal