485 lines
14 KiB
Go
485 lines
14 KiB
Go
package simulation
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/binary"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"io"
|
|
"math/rand"
|
|
"testing"
|
|
"time"
|
|
|
|
abci "github.com/cometbft/cometbft/abci/types"
|
|
cmtproto "github.com/cometbft/cometbft/proto/tendermint/types"
|
|
|
|
"cosmossdk.io/core/header"
|
|
"cosmossdk.io/log"
|
|
|
|
"github.com/cosmos/cosmos-sdk/baseapp"
|
|
"github.com/cosmos/cosmos-sdk/codec"
|
|
sdk "github.com/cosmos/cosmos-sdk/types"
|
|
"github.com/cosmos/cosmos-sdk/types/simulation"
|
|
)
|
|
|
|
const AverageBlockTime = 6 * time.Second
|
|
|
|
// initialize the chain for the simulation
|
|
func initChain(
|
|
r *rand.Rand,
|
|
params Params,
|
|
accounts []simulation.Account,
|
|
app *baseapp.BaseApp,
|
|
appStateFn simulation.AppStateFn,
|
|
config simulation.Config,
|
|
cdc codec.JSONCodec,
|
|
) (mockValidators, time.Time, []simulation.Account, string) {
|
|
blockMaxGas := int64(-1)
|
|
if config.BlockMaxGas > 0 {
|
|
blockMaxGas = config.BlockMaxGas
|
|
}
|
|
appState, accounts, chainID, genesisTimestamp := appStateFn(r, accounts, config)
|
|
consensusParams := randomConsensusParams(r, appState, cdc, blockMaxGas)
|
|
req := abci.RequestInitChain{
|
|
AppStateBytes: appState,
|
|
ChainId: chainID,
|
|
ConsensusParams: consensusParams,
|
|
Time: genesisTimestamp,
|
|
}
|
|
res, err := app.InitChain(&req)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
validators := newMockValidators(r, res.Validators, params)
|
|
|
|
return validators, genesisTimestamp, accounts, chainID
|
|
}
|
|
|
|
// SimulateFromSeed tests an application by running the provided
|
|
// operations, testing the provided invariants, but using the provided config.Seed.
|
|
func SimulateFromSeed(
|
|
tb testing.TB,
|
|
w io.Writer,
|
|
app *baseapp.BaseApp,
|
|
appStateFn simulation.AppStateFn,
|
|
randAccFn simulation.RandomAccountFn,
|
|
ops WeightedOperations,
|
|
blockedAddrs map[string]bool,
|
|
config simulation.Config,
|
|
cdc codec.JSONCodec,
|
|
) (stopEarly bool, exportedParams Params, err error) {
|
|
tb.Helper()
|
|
mode, _, _ := getTestingMode(tb)
|
|
expParams, _, err := SimulateFromSeedX(tb, log.NewTestLogger(tb), w, app, appStateFn, randAccFn, ops, blockedAddrs, config, cdc, NewLogWriter(mode))
|
|
return false, expParams, err
|
|
}
|
|
|
|
// SimulateFromSeedX tests an application by running the provided
|
|
// operations, testing the provided invariants, but using the provided config.Seed.
|
|
func SimulateFromSeedX(
|
|
tb testing.TB,
|
|
logger log.Logger,
|
|
w io.Writer,
|
|
app *baseapp.BaseApp,
|
|
appStateFn simulation.AppStateFn,
|
|
randAccFn simulation.RandomAccountFn,
|
|
ops WeightedOperations,
|
|
blockedAddrs map[string]bool,
|
|
config simulation.Config,
|
|
cdc codec.JSONCodec,
|
|
logWriter LogWriter,
|
|
) (exportedParams Params, accs []simulation.Account, err error) {
|
|
tb.Helper()
|
|
// 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(newByteSource(config.FuzzSeed, config.Seed))
|
|
params := RandomParams(r)
|
|
|
|
startTime := time.Now()
|
|
logger.Info("Starting SimulateFromSeed with randomness", "time", startTime)
|
|
logger.Debug("Randomized simulation setup", "params", mustMarshalJSONIndent(params))
|
|
|
|
timeDiff := maxTimePerBlock - minTimePerBlock
|
|
accs = randAccFn(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, blockTime, accs, chainID := initChain(r, params, accs, app, appStateFn, config, cdc)
|
|
// At least 2 accounts must be added here, otherwise when executing SimulateMsgSend
|
|
// two accounts will be selected to meet the conditions from != to and it will fall into an infinite loop.
|
|
if len(accs) <= 1 {
|
|
return params, accs, fmt.Errorf("at least two genesis accounts are required")
|
|
}
|
|
|
|
config.ChainID = chainID
|
|
|
|
// remove module account address if they exist in accs
|
|
var tmpAccs []simulation.Account
|
|
|
|
for _, acc := range accs {
|
|
if !blockedAddrs[acc.Address.String()] {
|
|
tmpAccs = append(tmpAccs, acc)
|
|
}
|
|
}
|
|
|
|
accs = tmpAccs
|
|
nextValidators := validators
|
|
if len(nextValidators) == 0 {
|
|
tb.Skip("skipping: empty validator set in genesis")
|
|
return params, accs, nil
|
|
}
|
|
|
|
var (
|
|
pastTimes []time.Time
|
|
pastVoteInfos [][]abci.VoteInfo
|
|
timeOperationQueue []simulation.FutureOperation
|
|
|
|
blockHeight = int64(config.InitialBlockHeight)
|
|
proposerAddress = validators.randomProposer(r)
|
|
opCount = 0
|
|
)
|
|
|
|
finalizeBlockReq := RandomRequestFinalizeBlock(
|
|
r,
|
|
params,
|
|
validators,
|
|
pastTimes,
|
|
pastVoteInfos,
|
|
eventStats.Tally,
|
|
blockHeight,
|
|
blockTime,
|
|
validators.randomProposer(r),
|
|
)
|
|
|
|
// These are operations which have been queued by previous operations
|
|
operationQueue := NewOperationQueue()
|
|
|
|
blockSimulator := createBlockSimulator(
|
|
tb,
|
|
testingMode,
|
|
w,
|
|
params,
|
|
eventStats.Tally,
|
|
ops,
|
|
operationQueue,
|
|
timeOperationQueue,
|
|
logWriter,
|
|
config,
|
|
)
|
|
|
|
if !testingMode {
|
|
b.ResetTimer()
|
|
} else {
|
|
// recover logs in case of panic
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
logger.Error("simulation halted due to panic", "height", blockHeight)
|
|
logWriter.PrintLogs()
|
|
panic(r)
|
|
}
|
|
}()
|
|
}
|
|
|
|
// set exported params to the initial state
|
|
if config.ExportParamsPath != "" && config.ExportParamsHeight == 0 {
|
|
exportedParams = params
|
|
}
|
|
|
|
for blockHeight < int64(config.NumBlocks+config.InitialBlockHeight) {
|
|
pastTimes = append(pastTimes, blockTime)
|
|
pastVoteInfos = append(pastVoteInfos, finalizeBlockReq.DecidedLastCommit.Votes)
|
|
|
|
// Run the BeginBlock handler
|
|
logWriter.AddEntry(BeginBlockEntry(blockHeight))
|
|
|
|
res, err := app.FinalizeBlock(finalizeBlockReq)
|
|
if err != nil {
|
|
return params, accs, fmt.Errorf("block finalization failed at height %d: %w", blockHeight, err)
|
|
}
|
|
|
|
ctx := app.NewContextLegacy(false, cmtproto.Header{
|
|
Height: blockHeight,
|
|
Time: blockTime,
|
|
ProposerAddress: proposerAddress,
|
|
ChainID: config.ChainID,
|
|
}).WithHeaderInfo(header.Info{
|
|
Height: blockHeight,
|
|
Time: blockTime,
|
|
ChainID: config.ChainID,
|
|
})
|
|
|
|
// run queued operations; ignores block size if block size is too small
|
|
numQueuedOpsRan, futureOps := runQueuedOperations(
|
|
tb, operationQueue, blockTime, int(blockHeight), r, app, ctx, accs, logWriter,
|
|
eventStats.Tally, config.Lean, config.ChainID,
|
|
)
|
|
|
|
numQueuedTimeOpsRan, timeFutureOps := runQueuedTimeOperations(tb,
|
|
timeOperationQueue, int(blockHeight), blockTime,
|
|
r, app, ctx, accs, logWriter, eventStats.Tally,
|
|
config.Lean, config.ChainID,
|
|
)
|
|
|
|
futureOps = append(futureOps, timeFutureOps...)
|
|
queueOperations(operationQueue, timeOperationQueue, futureOps)
|
|
|
|
// run standard operations
|
|
operations := blockSimulator(r, app, ctx, accs, cmtproto.Header{
|
|
Height: blockHeight,
|
|
Time: blockTime,
|
|
ProposerAddress: proposerAddress,
|
|
ChainID: config.ChainID,
|
|
})
|
|
opCount += operations + numQueuedOpsRan + numQueuedTimeOpsRan
|
|
|
|
blockHeight++
|
|
|
|
logWriter.AddEntry(EndBlockEntry(blockHeight))
|
|
|
|
blockTime = blockTime.Add(time.Duration(minTimePerBlock) * time.Second)
|
|
blockTime = blockTime.Add(time.Duration(int64(r.Intn(int(timeDiff)))) * time.Second)
|
|
proposerAddress = validators.randomProposer(r)
|
|
|
|
if config.Commit {
|
|
app.SimWriteState()
|
|
if _, err := app.Commit(); err != nil {
|
|
return params, accs, fmt.Errorf("commit failed at height %d: %w", blockHeight, err)
|
|
}
|
|
}
|
|
|
|
if proposerAddress == nil {
|
|
logger.Info("Simulation stopped early as all validators have been unbonded; nobody left to propose a block", "height", blockHeight)
|
|
break
|
|
}
|
|
|
|
// Generate a random RequestBeginBlock with the current validator set
|
|
// for the next block
|
|
finalizeBlockReq = RandomRequestFinalizeBlock(r, params, validators, pastTimes, pastVoteInfos, eventStats.Tally, blockHeight, blockTime, proposerAddress)
|
|
|
|
// 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 len(nextValidators) == 0 {
|
|
tb.Skip("skipping: empty validator set")
|
|
return params, accs, nil
|
|
}
|
|
|
|
// update the exported params
|
|
if config.ExportParamsPath != "" && int64(config.ExportParamsHeight) == blockHeight {
|
|
exportedParams = params
|
|
}
|
|
}
|
|
|
|
logger.Info("Simulation complete", "height", blockHeight, "block-time", blockTime, "opsCount", opCount,
|
|
"run-time", time.Since(startTime), "app-hash", hex.EncodeToString(app.LastCommitID().Hash))
|
|
|
|
if config.ExportStatsPath != "" {
|
|
fmt.Println("Exporting simulation statistics...")
|
|
eventStats.ExportJSON(config.ExportStatsPath)
|
|
} else {
|
|
eventStats.Print(w)
|
|
}
|
|
return exportedParams, accs, err
|
|
}
|
|
|
|
type blockSimFn func(
|
|
r *rand.Rand,
|
|
app *baseapp.BaseApp,
|
|
ctx sdk.Context,
|
|
accounts []simulation.Account,
|
|
header cmtproto.Header,
|
|
) (opCount int)
|
|
|
|
// Returns a function to simulate blocks. Written like this to avoid constant
|
|
// parameters being passed every time, to minimize memory overhead.
|
|
func createBlockSimulator(tb testing.TB, printProgress bool, w io.Writer, params Params,
|
|
event func(route, op, evResult string), ops WeightedOperations,
|
|
operationQueue OperationQueue, timeOperationQueue []simulation.FutureOperation,
|
|
logWriter LogWriter, config simulation.Config,
|
|
) blockSimFn {
|
|
tb.Helper()
|
|
lastBlockSizeState := 0 // state for [4 * uniform distribution]
|
|
blocksize := 0
|
|
selectOp := ops.getSelectOpFn()
|
|
|
|
return func(
|
|
r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accounts []simulation.Account, header cmtproto.Header,
|
|
) (opCount int) {
|
|
_, _ = fmt.Fprintf(
|
|
w, "\rSimulating... block %d/%d, operation %d/%d.",
|
|
header.Height, config.NumBlocks, opCount, blocksize,
|
|
)
|
|
lastBlockSizeState, blocksize = getBlockSize(r, params, lastBlockSizeState, config.BlockSize)
|
|
|
|
type opAndR struct {
|
|
op simulation.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 range blocksize {
|
|
opAndRz = append(opAndRz, opAndR{
|
|
op: selectOp(r),
|
|
rand: r,
|
|
})
|
|
}
|
|
|
|
for i := range blocksize {
|
|
// NOTE: the Rand 'r' should not be used here.
|
|
opAndR := opAndRz[i]
|
|
op, r2 := opAndR.op, opAndR.rand
|
|
opMsg, futureOps, err := op(r2, app, ctx, accounts, config.ChainID)
|
|
opMsg.LogEvent(event)
|
|
|
|
if !config.Lean || opMsg.OK {
|
|
logWriter.AddEntry(MsgEntry(header.Height, int64(i), opMsg))
|
|
}
|
|
|
|
if err != nil {
|
|
logWriter.PrintLogs()
|
|
tb.Fatalf(`error on block %d/%d, operation (%d/%d) from x/%s for msg %q:
|
|
%v
|
|
Comment: %s`,
|
|
header.Height, config.NumBlocks, opCount, blocksize, opMsg.Route, opMsg.Name, err, opMsg.Comment)
|
|
}
|
|
|
|
queueOperations(operationQueue, timeOperationQueue, futureOps)
|
|
|
|
if printProgress && opCount%50 == 0 {
|
|
_, _ = fmt.Fprintf(w, "\rSimulating... block %d/%d, operation %d/%d. ",
|
|
header.Height, config.NumBlocks, opCount, blocksize)
|
|
}
|
|
|
|
opCount++
|
|
}
|
|
|
|
return opCount
|
|
}
|
|
}
|
|
|
|
func runQueuedOperations(
|
|
tb testing.TB,
|
|
queueOps map[int][]simulation.Operation,
|
|
blockTime time.Time,
|
|
height int,
|
|
r *rand.Rand,
|
|
app *baseapp.BaseApp,
|
|
ctx sdk.Context,
|
|
accounts []simulation.Account,
|
|
logWriter LogWriter,
|
|
event func(route, op, evResult string),
|
|
lean bool,
|
|
chainID string,
|
|
) (numOpsRan int, allFutureOps []simulation.FutureOperation) {
|
|
tb.Helper()
|
|
queuedOp, ok := queueOps[height]
|
|
if !ok {
|
|
return 0, nil
|
|
}
|
|
|
|
// Keep all future operations
|
|
allFutureOps = make([]simulation.FutureOperation, 0)
|
|
|
|
numOpsRan = len(queuedOp)
|
|
for i := range numOpsRan {
|
|
opMsg, futureOps, err := queuedOp[i](r, app, ctx, accounts, chainID)
|
|
if len(futureOps) > 0 {
|
|
allFutureOps = append(allFutureOps, futureOps...)
|
|
}
|
|
|
|
opMsg.LogEvent(event)
|
|
|
|
if !lean || opMsg.OK {
|
|
logWriter.AddEntry((QueuedMsgEntry(int64(height), opMsg)))
|
|
}
|
|
|
|
if err != nil {
|
|
logWriter.PrintLogs()
|
|
tb.FailNow()
|
|
}
|
|
}
|
|
delete(queueOps, height)
|
|
|
|
return numOpsRan, allFutureOps
|
|
}
|
|
|
|
func runQueuedTimeOperations(tb testing.TB, queueOps []simulation.FutureOperation,
|
|
height int, currentTime time.Time, r *rand.Rand,
|
|
app *baseapp.BaseApp, ctx sdk.Context, accounts []simulation.Account,
|
|
logWriter LogWriter, event func(route, op, evResult string),
|
|
lean bool, chainID string,
|
|
) (numOpsRan int, allFutureOps []simulation.FutureOperation) {
|
|
tb.Helper()
|
|
// Keep all future operations
|
|
allFutureOps = make([]simulation.FutureOperation, 0)
|
|
|
|
numOpsRan = 0
|
|
for len(queueOps) > 0 && currentTime.After(queueOps[0].BlockTime) {
|
|
opMsg, futureOps, err := queueOps[0].Op(r, app, ctx, accounts, chainID)
|
|
|
|
opMsg.LogEvent(event)
|
|
|
|
if !lean || opMsg.OK {
|
|
logWriter.AddEntry(QueuedMsgEntry(int64(height), opMsg))
|
|
}
|
|
|
|
if err != nil {
|
|
logWriter.PrintLogs()
|
|
tb.FailNow()
|
|
}
|
|
|
|
if len(futureOps) > 0 {
|
|
allFutureOps = append(allFutureOps, futureOps...)
|
|
}
|
|
|
|
queueOps = queueOps[1:]
|
|
numOpsRan++
|
|
}
|
|
|
|
return numOpsRan, allFutureOps
|
|
}
|
|
|
|
const (
|
|
rngMax = 1 << 63
|
|
rngMask = rngMax - 1
|
|
)
|
|
|
|
// byteSource offers deterministic pseudo-random numbers for math.Rand with fuzzer support.
|
|
// The 'seed' data is read in big endian to uint64. When exhausted,
|
|
// it falls back to a standard random number generator initialized with a specific 'seed' value.
|
|
type byteSource struct {
|
|
seed *bytes.Reader
|
|
fallback *rand.Rand
|
|
}
|
|
|
|
// newByteSource creates a new byteSource with a specified byte slice and seed. This gives a fixed sequence of pseudo-random numbers.
|
|
// Initially, it utilizes the byte slice. Once that's exhausted, it continues generating numbers using the provided seed.
|
|
func newByteSource(fuzzSeed []byte, seed int64) *byteSource {
|
|
return &byteSource{
|
|
seed: bytes.NewReader(fuzzSeed),
|
|
fallback: rand.New(rand.NewSource(seed)),
|
|
}
|
|
}
|
|
|
|
func (s *byteSource) Uint64() uint64 {
|
|
if s.seed.Len() < 8 {
|
|
return s.fallback.Uint64()
|
|
}
|
|
var b [8]byte
|
|
if _, err := s.seed.Read(b[:]); err != nil && err != io.EOF {
|
|
panic(err) // Should not happen.
|
|
}
|
|
return binary.BigEndian.Uint64(b[:])
|
|
}
|
|
|
|
func (s *byteSource) Int63() int64 {
|
|
return int64(s.Uint64() & rngMask)
|
|
}
|
|
func (s *byteSource) Seed(seed int64) {}
|