github.com/fibonacci-chain/fbc@v0.0.0-20231124064014-c7636198c1e9/libs/cosmos-sdk/x/simulation/simulate.go (about)

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