From 59b67860d76d9a4b62b478bfd30944a2d937cb7b Mon Sep 17 00:00:00 2001 From: ggrieco-tob Date: Thu, 4 Jan 2024 17:37:41 +0100 Subject: [PATCH] proof-of-concept of using the standard go fuzzing for executing the simapp simulation --- simapp/sim_test.go | 99 +++++++++++++++++++++++++++++++++++ types/simulation/config.go | 1 + types/simulation/rand_util.go | 9 +--- x/simulation/simulate.go | 47 +++++++++++++++-- x/staking/types/validator.go | 3 ++ 5 files changed, 148 insertions(+), 11 deletions(-) diff --git a/simapp/sim_test.go b/simapp/sim_test.go index 1299836bc7ed..fafb34f00128 100644 --- a/simapp/sim_test.go +++ b/simapp/sim_test.go @@ -9,6 +9,7 @@ import ( "runtime/debug" "strings" "testing" + "encoding/binary" abci "github.com/cometbft/cometbft/abci/types" cmtproto "github.com/cometbft/cometbft/proto/tendermint/types" @@ -56,6 +57,104 @@ func interBlockCacheOpt() func(*baseapp.BaseApp) { return baseapp.SetInterBlockCache(store.NewCommitKVStoreCacheManager()) } +func FuzzFullAppSimulation(f *testing.F) { + f.Fuzz(func(t *testing.T, raw_seeds [] byte) { + config := simcli.NewConfigFromFlags() + var seeds []int64 + for { + if (len(raw_seeds) < 8) { + break + } + + seeds = append(seeds, int64(binary.BigEndian.Uint64(raw_seeds))) + raw_seeds = raw_seeds[8:] + } + + if len(seeds) == 0 { + return + } + + config.Seeds = seeds + config.ChainID = SimAppChainID + + appOptions := make(simtestutil.AppOptionsMap, 0) + appOptions[flags.FlagHome] = DefaultNodeHome + appOptions[server.FlagInvCheckPeriod] = simcli.FlagPeriodValue + db := dbm.NewMemDB() + logger := log.NewNopLogger() + + app := NewSimApp(logger, db, nil, true, appOptions, interBlockCacheOpt(), baseapp.SetChainID(SimAppChainID)) + require.Equal(t, "SimApp", app.Name()) + + defer func() { + if r := recover(); r != nil { + err := fmt.Sprintf("%v", r) + shouldPanic := true + if strings.Contains(err, "validator set is empty after InitGenesis") { + shouldPanic = false + } + + if strings.Contains(err, "duplicate account in genesis state") { + shouldPanic = false + } + + if strings.Contains(err, "group policies: unique constraint violation") { + shouldPanic = false + } + + if strings.Contains(err, "nft class already exists") { + shouldPanic = false + } + + if strings.Contains(err, "nft already exists") { + shouldPanic = false + } + + if strings.Contains(err, "invalid coins") { + shouldPanic = false + } + + if strings.Contains(err, "invalid argument to Int63n") { + shouldPanic = false + } + + if strings.Contains(err, "v.DelegatorShares is zero") { + shouldPanic = false + } + + if strings.Contains(err, "group: not found") { + shouldPanic = false + } + + if shouldPanic { + panic(r) + } + + logger.Info("Skipping simulation as all validators have been unbonded") + logger.Info("err", err, "stacktrace", string(debug.Stack())) + } + }() + + // run randomized simulation + _,_, err := simulation.SimulateFromSeed( + t, + os.Stdout, + app.BaseApp, + simtestutil.AppStateFn(app.AppCodec(), app.SimulationManager(), app.DefaultGenesis()), + simtypes.RandomAccounts, // Replace with own random account function if using keys other than secp256k1 + simtestutil.SimulationOperations(app, app.AppCodec(), config), + BlockedAddresses(), + config, + app.AppCodec(), + ) + + if err != nil { + panic(err) + } + }) +} + + func TestFullAppSimulation(t *testing.T) { config := simcli.NewConfigFromFlags() config.ChainID = SimAppChainID diff --git a/types/simulation/config.go b/types/simulation/config.go index d2bb114133e0..d6b37d9efa44 100644 --- a/types/simulation/config.go +++ b/types/simulation/config.go @@ -11,6 +11,7 @@ type Config struct { ExportStatsPath string // custom file path to save the exported simulation statistics JSON Seed int64 // simulation random seed + Seeds [] int64 // Added InitialBlockHeight int // initial block to start the simulation GenesisTime int64 // genesis time to start the simulation NumBlocks int // number of new blocks to simulate from the initial block height diff --git a/types/simulation/rand_util.go b/types/simulation/rand_util.go index adacd90ad436..405de06ac507 100644 --- a/types/simulation/rand_util.go +++ b/types/simulation/rand_util.go @@ -152,14 +152,7 @@ func RandSubsetCoins(r *rand.Rand, coins sdk.Coins) sdk.Coins { // // 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) + return r } type multiSource []rand.Source diff --git a/x/simulation/simulate.go b/x/simulation/simulate.go index 1da741088559..daa812044303 100644 --- a/x/simulation/simulate.go +++ b/x/simulation/simulate.go @@ -23,6 +23,39 @@ import ( const AverageBlockTime = 6 * time.Second +const ( + rngMax = 1 << 63 + rngMask = rngMax - 1 +) + +type arraySource struct { + pos int + arr []int64 + src *rand.Rand +} + +// Int63 returns a non-negative pseudo-random 63-bit integer as an int64. +func (rng *arraySource) Int63() int64 { + return int64(rng.Uint64() & rngMask) +} + +// Uint64 returns a non-negative pseudo-random 64-bit integer as an uint64. +func (rng *arraySource) Uint64() uint64 { + if (rng.pos >= len(rng.arr)) { + return rng.src.Uint64() + } + val := rng.arr[rng.pos] + rng.pos = rng.pos + 1 + if val < 0 { + return uint64(-val) + } + + return uint64(val) +} + +func (rng *arraySource) Seed(seed int64) {} + + // initialize the chain for the simulation func initChain( r *rand.Rand, @@ -71,7 +104,12 @@ func SimulateFromSeed( // in case we have to end early, don't os.Exit so that we can run cleanup code. testingMode, _, b := getTestingMode(tb) - r := rand.New(rand.NewSource(config.Seed)) + //r := rand.New(rand.NewSource(config.Seed)) + var rng arraySource + rng.arr = config.Seeds + rng.src = rand.New(rand.NewSource(config.Seed)) + r := rand.New(&rng) + params := RandomParams(r) fmt.Fprintf(w, "Starting SimulateFromSeed with randomness created with seed %d\n", int(config.Seed)) @@ -339,6 +377,7 @@ func createBlockSimulator(tb testing.TB, testingMode bool, w io.Writer, params P op, r2 := opAndR.op, opAndR.rand opMsg, futureOps, err := op(r2, app, ctx, accounts, config.ChainID) opMsg.LogEvent(event) + fmt.Printf("\n Executing: %s\n", opMsg.String()) if !config.Lean || opMsg.OK { logWriter.AddEntry(MsgEntry(header.Height, int64(i), opMsg)) @@ -346,10 +385,12 @@ func createBlockSimulator(tb testing.TB, testingMode bool, w io.Writer, params P if err != nil { logWriter.PrintLogs() - tb.Fatalf(`error on block %d/%d, operation (%d/%d) from x/%s: + if opMsg.OK { + tb.Fatalf(`error on block %d/%d, operation (%d/%d) from x/%s: %v Comment: %s`, - header.Height, config.NumBlocks, opCount, blocksize, opMsg.Route, err, opMsg.Comment) + header.Height, config.NumBlocks, opCount, blocksize, opMsg.Route, err, opMsg.String()) + } } queueOperations(operationQueue, timeOperationQueue, futureOps) diff --git a/x/staking/types/validator.go b/x/staking/types/validator.go index f51d4e3990e7..9af5512e7ae3 100644 --- a/x/staking/types/validator.go +++ b/x/staking/types/validator.go @@ -310,6 +310,9 @@ func (v Validator) TokensFromShares(shares math.LegacyDec) math.LegacyDec { // calculate the token worth of provided shares, truncated func (v Validator) TokensFromSharesTruncated(shares math.LegacyDec) math.LegacyDec { + if (v.DelegatorShares.IsZero()) { + panic("v.DelegatorShares is zero"); + } return (shares.MulInt(v.Tokens)).QuoTruncate(v.DelegatorShares) }