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