github.com/cosmos/cosmos-sdk@v0.50.10/x/simulation/simulate.go (about)

     1  package simulation
     2  
     3  import (
     4  	"fmt"
     5  	"io"
     6  	"math/rand"
     7  	"os"
     8  	"os/signal"
     9  	"syscall"
    10  	"testing"
    11  	"time"
    12  
    13  	abci "github.com/cometbft/cometbft/abci/types"
    14  	cmtproto "github.com/cometbft/cometbft/proto/tendermint/types"
    15  
    16  	"github.com/cosmos/cosmos-sdk/baseapp"
    17  	"github.com/cosmos/cosmos-sdk/codec"
    18  	sdk "github.com/cosmos/cosmos-sdk/types"
    19  	"github.com/cosmos/cosmos-sdk/types/simulation"
    20  )
    21  
    22  const AverageBlockTime = 6 * time.Second
    23  
    24  // initialize the chain for the simulation
    25  func initChain(
    26  	r *rand.Rand,
    27  	params Params,
    28  	accounts []simulation.Account,
    29  	app *baseapp.BaseApp,
    30  	appStateFn simulation.AppStateFn,
    31  	config simulation.Config,
    32  	cdc codec.JSONCodec,
    33  ) (mockValidators, time.Time, []simulation.Account, string) {
    34  	blockMaxGas := int64(-1)
    35  	if config.BlockMaxGas > 0 {
    36  		blockMaxGas = config.BlockMaxGas
    37  	}
    38  	appState, accounts, chainID, genesisTimestamp := appStateFn(r, accounts, config)
    39  	consensusParams := randomConsensusParams(r, appState, cdc, blockMaxGas)
    40  	req := abci.RequestInitChain{
    41  		AppStateBytes:   appState,
    42  		ChainId:         chainID,
    43  		ConsensusParams: consensusParams,
    44  		Time:            genesisTimestamp,
    45  	}
    46  	res, err := app.InitChain(&req)
    47  	if err != nil {
    48  		panic(err)
    49  	}
    50  	validators := newMockValidators(r, res.Validators, params)
    51  
    52  	return validators, genesisTimestamp, accounts, chainID
    53  }
    54  
    55  // SimulateFromSeed tests an application by running the provided
    56  // operations, testing the provided invariants, but using the provided config.Seed.
    57  func SimulateFromSeed(
    58  	tb testing.TB,
    59  	w io.Writer,
    60  	app *baseapp.BaseApp,
    61  	appStateFn simulation.AppStateFn,
    62  	randAccFn simulation.RandomAccountFn,
    63  	ops WeightedOperations,
    64  	blockedAddrs map[string]bool,
    65  	config simulation.Config,
    66  	cdc codec.JSONCodec,
    67  ) (stopEarly bool, exportedParams Params, err error) {
    68  	// in case we have to end early, don't os.Exit so that we can run cleanup code.
    69  	testingMode, _, b := getTestingMode(tb)
    70  
    71  	r := rand.New(rand.NewSource(config.Seed))
    72  	params := RandomParams(r)
    73  
    74  	fmt.Fprintf(w, "Starting SimulateFromSeed with randomness created with seed %d\n", int(config.Seed))
    75  	fmt.Fprintf(w, "Randomized simulation params: \n%s\n", mustMarshalJSONIndent(params))
    76  
    77  	timeDiff := maxTimePerBlock - minTimePerBlock
    78  	accs := randAccFn(r, params.NumKeys())
    79  	eventStats := NewEventStats()
    80  
    81  	// Second variable to keep pending validator set (delayed one block since
    82  	// TM 0.24) Initially this is the same as the initial validator set
    83  	validators, blockTime, accs, chainID := initChain(r, params, accs, app, appStateFn, config, cdc)
    84  	// At least 2 accounts must be added here, otherwise when executing SimulateMsgSend
    85  	// two accounts will be selected to meet the conditions from != to and it will fall into an infinite loop.
    86  	if len(accs) <= 1 {
    87  		return true, params, fmt.Errorf("at least two genesis accounts are required")
    88  	}
    89  
    90  	config.ChainID = chainID
    91  
    92  	fmt.Printf(
    93  		"Starting the simulation from time %v (unixtime %v)\n",
    94  		blockTime.UTC().Format(time.UnixDate), blockTime.Unix(),
    95  	)
    96  
    97  	// remove module account address if they exist in accs
    98  	var tmpAccs []simulation.Account
    99  
   100  	for _, acc := range accs {
   101  		if !blockedAddrs[acc.Address.String()] {
   102  			tmpAccs = append(tmpAccs, acc)
   103  		}
   104  	}
   105  
   106  	accs = tmpAccs
   107  	nextValidators := validators
   108  
   109  	var (
   110  		pastTimes          []time.Time
   111  		pastVoteInfos      [][]abci.VoteInfo
   112  		timeOperationQueue []simulation.FutureOperation
   113  
   114  		blockHeight     = int64(config.InitialBlockHeight)
   115  		proposerAddress = validators.randomProposer(r)
   116  		opCount         = 0
   117  	)
   118  
   119  	// Setup code to catch SIGTERM's
   120  	c := make(chan os.Signal, 1)
   121  	signal.Notify(c, os.Interrupt, syscall.SIGTERM, syscall.SIGINT)
   122  
   123  	go func() {
   124  		receivedSignal := <-c
   125  		fmt.Fprintf(w, "\nExiting early due to %s, on block %d, operation %d\n", receivedSignal, blockHeight, opCount)
   126  		err = fmt.Errorf("exited due to %s", receivedSignal)
   127  		stopEarly = true
   128  	}()
   129  
   130  	finalizeBlockReq := RandomRequestFinalizeBlock(
   131  		r,
   132  		params,
   133  		validators,
   134  		pastTimes,
   135  		pastVoteInfos,
   136  		eventStats.Tally,
   137  		blockHeight,
   138  		blockTime,
   139  		validators.randomProposer(r),
   140  	)
   141  
   142  	// These are operations which have been queued by previous operations
   143  	operationQueue := NewOperationQueue()
   144  	logWriter := NewLogWriter(testingMode)
   145  
   146  	blockSimulator := createBlockSimulator(
   147  		testingMode,
   148  		tb,
   149  		w,
   150  		params,
   151  		eventStats.Tally,
   152  		ops,
   153  		operationQueue,
   154  		timeOperationQueue,
   155  		logWriter,
   156  		config,
   157  	)
   158  
   159  	if !testingMode {
   160  		b.ResetTimer()
   161  	} else {
   162  		// recover logs in case of panic
   163  		defer func() {
   164  			if r := recover(); r != nil {
   165  				_, _ = fmt.Fprintf(w, "simulation halted due to panic on block %d\n", blockHeight)
   166  				logWriter.PrintLogs()
   167  				panic(r)
   168  			}
   169  		}()
   170  	}
   171  
   172  	// set exported params to the initial state
   173  	if config.ExportParamsPath != "" && config.ExportParamsHeight == 0 {
   174  		exportedParams = params
   175  	}
   176  
   177  	for blockHeight < int64(config.NumBlocks+config.InitialBlockHeight) && !stopEarly {
   178  		pastTimes = append(pastTimes, blockTime)
   179  		pastVoteInfos = append(pastVoteInfos, finalizeBlockReq.DecidedLastCommit.Votes)
   180  
   181  		// Run the BeginBlock handler
   182  		logWriter.AddEntry(BeginBlockEntry(blockHeight))
   183  
   184  		res, err := app.FinalizeBlock(finalizeBlockReq)
   185  		if err != nil {
   186  			return true, params, err
   187  		}
   188  
   189  		ctx := app.NewContextLegacy(false, cmtproto.Header{
   190  			Height:          blockHeight,
   191  			Time:            blockTime,
   192  			ProposerAddress: proposerAddress,
   193  			ChainID:         config.ChainID,
   194  		})
   195  
   196  		// run queued operations; ignores block size if block size is too small
   197  		numQueuedOpsRan, futureOps := runQueuedOperations(
   198  			operationQueue, int(blockHeight), tb, r, app, ctx, accs, logWriter,
   199  			eventStats.Tally, config.Lean, config.ChainID,
   200  		)
   201  
   202  		numQueuedTimeOpsRan, timeFutureOps := runQueuedTimeOperations(
   203  			timeOperationQueue, int(blockHeight), blockTime,
   204  			tb, r, app, ctx, accs, logWriter, eventStats.Tally,
   205  			config.Lean, config.ChainID,
   206  		)
   207  
   208  		futureOps = append(futureOps, timeFutureOps...)
   209  		queueOperations(operationQueue, timeOperationQueue, futureOps)
   210  
   211  		// run standard operations
   212  		operations := blockSimulator(r, app, ctx, accs, cmtproto.Header{
   213  			Height:          blockHeight,
   214  			Time:            blockTime,
   215  			ProposerAddress: proposerAddress,
   216  			ChainID:         config.ChainID,
   217  		})
   218  		opCount += operations + numQueuedOpsRan + numQueuedTimeOpsRan
   219  
   220  		blockHeight++
   221  
   222  		blockTime = blockTime.Add(time.Duration(minTimePerBlock) * time.Second)
   223  		blockTime = blockTime.Add(time.Duration(int64(r.Intn(int(timeDiff)))) * time.Second)
   224  		proposerAddress = validators.randomProposer(r)
   225  
   226  		logWriter.AddEntry(EndBlockEntry(blockHeight))
   227  
   228  		if config.Commit {
   229  			app.Commit()
   230  		}
   231  
   232  		if proposerAddress == nil {
   233  			fmt.Fprintf(w, "\nSimulation stopped early as all validators have been unbonded; nobody left to propose a block!\n")
   234  			stopEarly = true
   235  			break
   236  		}
   237  
   238  		// Generate a random RequestBeginBlock with the current validator set
   239  		// for the next block
   240  		finalizeBlockReq = RandomRequestFinalizeBlock(r, params, validators, pastTimes, pastVoteInfos, eventStats.Tally, blockHeight, blockTime, proposerAddress)
   241  
   242  		// Update the validator set, which will be reflected in the application
   243  		// on the next block
   244  		validators = nextValidators
   245  		nextValidators = updateValidators(tb, r, params, validators, res.ValidatorUpdates, eventStats.Tally)
   246  
   247  		// update the exported params
   248  		if config.ExportParamsPath != "" && int64(config.ExportParamsHeight) == blockHeight {
   249  			exportedParams = params
   250  		}
   251  	}
   252  
   253  	if stopEarly {
   254  		if config.ExportStatsPath != "" {
   255  			fmt.Println("Exporting simulation statistics...")
   256  			eventStats.ExportJSON(config.ExportStatsPath)
   257  		} else {
   258  			eventStats.Print(w)
   259  		}
   260  
   261  		return true, exportedParams, err
   262  	}
   263  
   264  	fmt.Fprintf(
   265  		w,
   266  		"\nSimulation complete; Final height (blocks): %d, final time (seconds): %v, operations ran: %d\n",
   267  		blockHeight, blockTime, opCount,
   268  	)
   269  
   270  	if config.ExportStatsPath != "" {
   271  		fmt.Println("Exporting simulation statistics...")
   272  		eventStats.ExportJSON(config.ExportStatsPath)
   273  	} else {
   274  		eventStats.Print(w)
   275  	}
   276  
   277  	return false, exportedParams, nil
   278  }
   279  
   280  type blockSimFn func(
   281  	r *rand.Rand,
   282  	app *baseapp.BaseApp,
   283  	ctx sdk.Context,
   284  	accounts []simulation.Account,
   285  	header cmtproto.Header,
   286  ) (opCount int)
   287  
   288  // Returns a function to simulate blocks. Written like this to avoid constant
   289  // parameters being passed everytime, to minimize memory overhead.
   290  func createBlockSimulator(testingMode bool, tb testing.TB, w io.Writer, params Params,
   291  	event func(route, op, evResult string), ops WeightedOperations,
   292  	operationQueue OperationQueue, timeOperationQueue []simulation.FutureOperation,
   293  	logWriter LogWriter, config simulation.Config,
   294  ) blockSimFn {
   295  	lastBlockSizeState := 0 // state for [4 * uniform distribution]
   296  	blocksize := 0
   297  	selectOp := ops.getSelectOpFn()
   298  
   299  	return func(
   300  		r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accounts []simulation.Account, header cmtproto.Header,
   301  	) (opCount int) {
   302  		_, _ = fmt.Fprintf(
   303  			w, "\rSimulating... block %d/%d, operation %d/%d.",
   304  			header.Height, config.NumBlocks, opCount, blocksize,
   305  		)
   306  		lastBlockSizeState, blocksize = getBlockSize(r, params, lastBlockSizeState, config.BlockSize)
   307  
   308  		type opAndR struct {
   309  			op   simulation.Operation
   310  			rand *rand.Rand
   311  		}
   312  
   313  		opAndRz := make([]opAndR, 0, blocksize)
   314  
   315  		// Predetermine the blocksize slice so that we can do things like block
   316  		// out certain operations without changing the ops that follow.
   317  		for i := 0; i < blocksize; i++ {
   318  			opAndRz = append(opAndRz, opAndR{
   319  				op:   selectOp(r),
   320  				rand: simulation.DeriveRand(r),
   321  			})
   322  		}
   323  
   324  		for i := 0; i < blocksize; i++ {
   325  			// NOTE: the Rand 'r' should not be used here.
   326  			opAndR := opAndRz[i]
   327  			op, r2 := opAndR.op, opAndR.rand
   328  			opMsg, futureOps, err := op(r2, app, ctx, accounts, config.ChainID)
   329  			opMsg.LogEvent(event)
   330  
   331  			if !config.Lean || opMsg.OK {
   332  				logWriter.AddEntry(MsgEntry(header.Height, int64(i), opMsg))
   333  			}
   334  
   335  			if err != nil {
   336  				logWriter.PrintLogs()
   337  				tb.Fatalf(`error on block  %d/%d, operation (%d/%d) from x/%s:
   338  %v
   339  Comment: %s`,
   340  					header.Height, config.NumBlocks, opCount, blocksize, opMsg.Route, err, opMsg.Comment)
   341  			}
   342  
   343  			queueOperations(operationQueue, timeOperationQueue, futureOps)
   344  
   345  			if testingMode && opCount%50 == 0 {
   346  				fmt.Fprintf(w, "\rSimulating... block %d/%d, operation %d/%d. ",
   347  					header.Height, config.NumBlocks, opCount, blocksize)
   348  			}
   349  
   350  			opCount++
   351  		}
   352  
   353  		return opCount
   354  	}
   355  }
   356  
   357  func runQueuedOperations(queueOps map[int][]simulation.Operation,
   358  	height int, tb testing.TB, r *rand.Rand, app *baseapp.BaseApp,
   359  	ctx sdk.Context, accounts []simulation.Account, logWriter LogWriter,
   360  	event func(route, op, evResult string), lean bool, chainID string,
   361  ) (numOpsRan int, allFutureOps []simulation.FutureOperation) {
   362  	queuedOp, ok := queueOps[height]
   363  	if !ok {
   364  		return 0, nil
   365  	}
   366  
   367  	// Keep all future operations
   368  	allFutureOps = make([]simulation.FutureOperation, 0)
   369  
   370  	numOpsRan = len(queuedOp)
   371  	for i := 0; i < numOpsRan; i++ {
   372  		opMsg, futureOps, err := queuedOp[i](r, app, ctx, accounts, chainID)
   373  		if len(futureOps) > 0 {
   374  			allFutureOps = append(allFutureOps, futureOps...)
   375  		}
   376  
   377  		opMsg.LogEvent(event)
   378  
   379  		if !lean || opMsg.OK {
   380  			logWriter.AddEntry((QueuedMsgEntry(int64(height), opMsg)))
   381  		}
   382  
   383  		if err != nil {
   384  			logWriter.PrintLogs()
   385  			tb.FailNow()
   386  		}
   387  	}
   388  	delete(queueOps, height)
   389  
   390  	return numOpsRan, allFutureOps
   391  }
   392  
   393  func runQueuedTimeOperations(queueOps []simulation.FutureOperation,
   394  	height int, currentTime time.Time, tb testing.TB, r *rand.Rand,
   395  	app *baseapp.BaseApp, ctx sdk.Context, accounts []simulation.Account,
   396  	logWriter LogWriter, event func(route, op, evResult string),
   397  	lean bool, chainID string,
   398  ) (numOpsRan int, allFutureOps []simulation.FutureOperation) {
   399  	// Keep all future operations
   400  	allFutureOps = make([]simulation.FutureOperation, 0)
   401  
   402  	numOpsRan = 0
   403  	for len(queueOps) > 0 && currentTime.After(queueOps[0].BlockTime) {
   404  		opMsg, futureOps, err := queueOps[0].Op(r, app, ctx, accounts, chainID)
   405  
   406  		opMsg.LogEvent(event)
   407  
   408  		if !lean || opMsg.OK {
   409  			logWriter.AddEntry(QueuedMsgEntry(int64(height), opMsg))
   410  		}
   411  
   412  		if err != nil {
   413  			logWriter.PrintLogs()
   414  			tb.FailNow()
   415  		}
   416  
   417  		if len(futureOps) > 0 {
   418  			allFutureOps = append(allFutureOps, futureOps...)
   419  		}
   420  
   421  		queueOps = queueOps[1:]
   422  		numOpsRan++
   423  	}
   424  
   425  	return numOpsRan, allFutureOps
   426  }