Merge PR #3819: Simulation Refactor
This commit is contained in:
committed by
Christopher Goes
parent
48b6b3884f
commit
f0d1efa43c
@@ -0,0 +1,53 @@
|
||||
package simulation
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
|
||||
"github.com/tendermint/tendermint/crypto"
|
||||
"github.com/tendermint/tendermint/crypto/ed25519"
|
||||
"github.com/tendermint/tendermint/crypto/secp256k1"
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
)
|
||||
|
||||
// Account contains a privkey, pubkey, address tuple
|
||||
// eventually more useful data can be placed in here.
|
||||
// (e.g. number of coins)
|
||||
type Account struct {
|
||||
PrivKey crypto.PrivKey
|
||||
PubKey crypto.PubKey
|
||||
Address sdk.AccAddress
|
||||
}
|
||||
|
||||
// are two accounts equal
|
||||
func (acc Account) Equals(acc2 Account) bool {
|
||||
return acc.Address.Equals(acc2.Address)
|
||||
}
|
||||
|
||||
// RandomAcc pick a random account from an array
|
||||
func RandomAcc(r *rand.Rand, accs []Account) Account {
|
||||
return accs[r.Intn(
|
||||
len(accs),
|
||||
)]
|
||||
}
|
||||
|
||||
// RandomAccounts generates n random accounts
|
||||
func RandomAccounts(r *rand.Rand, n int) []Account {
|
||||
accs := make([]Account, n)
|
||||
for i := 0; i < n; i++ {
|
||||
// don't need that much entropy for simulation
|
||||
privkeySeed := make([]byte, 15)
|
||||
r.Read(privkeySeed)
|
||||
useSecp := r.Int63()%2 == 0
|
||||
if useSecp {
|
||||
accs[i].PrivKey = secp256k1.GenPrivKeySecp256k1(privkeySeed)
|
||||
} else {
|
||||
accs[i].PrivKey = ed25519.GenPrivKeyFromSecret(privkeySeed)
|
||||
}
|
||||
|
||||
accs[i].PubKey = accs[i].PrivKey.PubKey()
|
||||
accs[i].Address = sdk.AccAddress(accs[i].PubKey.Address())
|
||||
}
|
||||
|
||||
return accs
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
Package simulation implements a simulation framework for any state machine
|
||||
built on the SDK which utilizes auth.
|
||||
|
||||
It is primarily intended for fuzz testing the integration of modules. It will
|
||||
test that the provided operations are interoperable, and that the desired
|
||||
invariants hold. It can additionally be used to detect what the performance
|
||||
benchmarks in the system are, by using benchmarking mode and cpu / mem
|
||||
profiling. If it detects a failure, it provides the entire log of what was ran.
|
||||
|
||||
The simulator takes as input: a random seed, the set of operations to run, the
|
||||
invariants to test, and additional parameters to configure how long to run, and
|
||||
misc. parameters that affect simulation speed.
|
||||
|
||||
It is intended that every module provides a list of Operations which will
|
||||
randomly create and run a message / tx in a manner that is interesting to fuzz,
|
||||
and verify that the state transition was executed as expected. Each module
|
||||
should additionally provide methods to assert that the desired invariants hold.
|
||||
|
||||
Then to perform a randomized simulation, select the set of desired operations,
|
||||
the weightings for each, the invariants you want to test, and how long to run
|
||||
it for. Then run simulation.Simulate! The simulator will handle things like
|
||||
ensuring that validators periodically double signing, or go offline.
|
||||
*/
|
||||
package simulation
|
||||
@@ -0,0 +1,30 @@
|
||||
package simulation
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
)
|
||||
|
||||
type eventStats map[string]uint
|
||||
|
||||
func newEventStats() eventStats {
|
||||
events := make(map[string]uint)
|
||||
return events
|
||||
}
|
||||
|
||||
func (es eventStats) tally(eventDesc string) {
|
||||
es[eventDesc]++
|
||||
}
|
||||
|
||||
// Pretty-print events as a table
|
||||
func (es eventStats) Print() {
|
||||
var keys []string
|
||||
for key := range es {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
fmt.Printf("Event statistics: \n")
|
||||
for _, key := range keys {
|
||||
fmt.Printf(" % 60s => %d\n", key, es[key])
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package simulation
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"time"
|
||||
)
|
||||
|
||||
// log writter
|
||||
type LogWriter interface {
|
||||
AddEntry(OperationEntry)
|
||||
PrintLogs()
|
||||
}
|
||||
|
||||
// LogWriter - return a dummy or standard log writer given the testingmode
|
||||
func NewLogWriter(testingmode bool) LogWriter {
|
||||
if !testingmode {
|
||||
return &DummyLogWriter{}
|
||||
}
|
||||
return &StandardLogWriter{}
|
||||
}
|
||||
|
||||
// log writter
|
||||
type StandardLogWriter struct {
|
||||
OpEntries []OperationEntry `json:"op_entries"`
|
||||
}
|
||||
|
||||
// add an entry to the log writter
|
||||
func (lw *StandardLogWriter) AddEntry(opEntry OperationEntry) {
|
||||
lw.OpEntries = append(lw.OpEntries, opEntry)
|
||||
}
|
||||
|
||||
// PrintLogs - print the logs to a simulation file
|
||||
func (lw *StandardLogWriter) PrintLogs() {
|
||||
f := createLogFile()
|
||||
for i := 0; i < len(lw.OpEntries); i++ {
|
||||
writeEntry := fmt.Sprintf("%s\n", (lw.OpEntries[i]).MustMarshal())
|
||||
_, err := f.WriteString(writeEntry)
|
||||
if err != nil {
|
||||
panic("Failed to write logs to file")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func createLogFile() *os.File {
|
||||
var f *os.File
|
||||
fileName := fmt.Sprintf("%s.log", time.Now().Format("2006-01-02_15:04:05"))
|
||||
|
||||
folderPath := os.ExpandEnv("$HOME/.gaiad/simulations")
|
||||
filePath := path.Join(folderPath, fileName)
|
||||
|
||||
err := os.MkdirAll(folderPath, os.ModePerm)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
f, _ = os.Create(filePath)
|
||||
fmt.Printf("Logs to writing to %s\n", filePath)
|
||||
return f
|
||||
}
|
||||
|
||||
//_____________________
|
||||
// dummy log writter
|
||||
type DummyLogWriter struct{}
|
||||
|
||||
// do nothing
|
||||
func (lw *DummyLogWriter) AddEntry(_ OperationEntry) {}
|
||||
|
||||
// do nothing
|
||||
func (lw *DummyLogWriter) PrintLogs() {}
|
||||
@@ -0,0 +1,210 @@
|
||||
package simulation
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"sort"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
abci "github.com/tendermint/tendermint/abci/types"
|
||||
cmn "github.com/tendermint/tendermint/libs/common"
|
||||
tmtypes "github.com/tendermint/tendermint/types"
|
||||
)
|
||||
|
||||
type mockValidator struct {
|
||||
val abci.ValidatorUpdate
|
||||
livenessState int
|
||||
}
|
||||
|
||||
func (mv mockValidator) String() string {
|
||||
return fmt.Sprintf("mockValidator{%s:%X power:%v state:%v}",
|
||||
mv.val.PubKey.Type,
|
||||
mv.val.PubKey.Data,
|
||||
mv.val.Power,
|
||||
mv.livenessState)
|
||||
}
|
||||
|
||||
type mockValidators map[string]mockValidator
|
||||
|
||||
// get mockValidators from abci validators
|
||||
func newMockValidators(r *rand.Rand, abciVals []abci.ValidatorUpdate,
|
||||
params Params) mockValidators {
|
||||
|
||||
validators := make(mockValidators)
|
||||
for _, validator := range abciVals {
|
||||
str := fmt.Sprintf("%v", validator.PubKey)
|
||||
liveliness := GetMemberOfInitialState(r,
|
||||
params.InitialLivenessWeightings)
|
||||
|
||||
validators[str] = mockValidator{
|
||||
val: validator,
|
||||
livenessState: liveliness,
|
||||
}
|
||||
}
|
||||
|
||||
return validators
|
||||
}
|
||||
|
||||
// TODO describe usage
|
||||
func (vals mockValidators) getKeys() []string {
|
||||
keys := make([]string, len(vals))
|
||||
i := 0
|
||||
for key := range vals {
|
||||
keys[i] = key
|
||||
i++
|
||||
}
|
||||
sort.Strings(keys)
|
||||
return keys
|
||||
}
|
||||
|
||||
//_________________________________________________________________________________
|
||||
|
||||
// randomProposer picks a random proposer from the current validator set
|
||||
func (vals mockValidators) randomProposer(r *rand.Rand) cmn.HexBytes {
|
||||
keys := vals.getKeys()
|
||||
if len(keys) == 0 {
|
||||
return nil
|
||||
}
|
||||
key := keys[r.Intn(len(keys))]
|
||||
proposer := vals[key].val
|
||||
pk, err := tmtypes.PB2TM.PubKey(proposer.PubKey)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return pk.Address()
|
||||
}
|
||||
|
||||
// updateValidators mimicks Tendermint's update logic
|
||||
// nolint: unparam
|
||||
func updateValidators(tb testing.TB, r *rand.Rand, params Params,
|
||||
current map[string]mockValidator, updates []abci.ValidatorUpdate,
|
||||
event func(string)) map[string]mockValidator {
|
||||
|
||||
for _, update := range updates {
|
||||
str := fmt.Sprintf("%v", update.PubKey)
|
||||
|
||||
if update.Power == 0 {
|
||||
if _, ok := current[str]; !ok {
|
||||
tb.Fatalf("tried to delete a nonexistent validator")
|
||||
}
|
||||
event("endblock/validatorupdates/kicked")
|
||||
delete(current, str)
|
||||
|
||||
} else if mVal, ok := current[str]; ok {
|
||||
// validator already exists
|
||||
mVal.val = update
|
||||
event("endblock/validatorupdates/updated")
|
||||
|
||||
} else {
|
||||
// Set this new validator
|
||||
current[str] = mockValidator{
|
||||
update,
|
||||
GetMemberOfInitialState(r, params.InitialLivenessWeightings),
|
||||
}
|
||||
event("endblock/validatorupdates/added")
|
||||
}
|
||||
}
|
||||
|
||||
return current
|
||||
}
|
||||
|
||||
// RandomRequestBeginBlock generates a list of signing validators according to
|
||||
// the provided list of validators, signing fraction, and evidence fraction
|
||||
func RandomRequestBeginBlock(r *rand.Rand, params Params,
|
||||
validators mockValidators, pastTimes []time.Time,
|
||||
pastVoteInfos [][]abci.VoteInfo,
|
||||
event func(string), header abci.Header) abci.RequestBeginBlock {
|
||||
|
||||
if len(validators) == 0 {
|
||||
return abci.RequestBeginBlock{
|
||||
Header: header,
|
||||
}
|
||||
}
|
||||
|
||||
voteInfos := make([]abci.VoteInfo, len(validators))
|
||||
for i, key := range validators.getKeys() {
|
||||
mVal := validators[key]
|
||||
mVal.livenessState = params.LivenessTransitionMatrix.NextState(r, mVal.livenessState)
|
||||
signed := true
|
||||
|
||||
if mVal.livenessState == 1 {
|
||||
// spotty connection, 50% probability of success
|
||||
// See https://github.com/golang/go/issues/23804#issuecomment-365370418
|
||||
// for reasoning behind computing like this
|
||||
signed = r.Int63()%2 == 0
|
||||
} else if mVal.livenessState == 2 {
|
||||
// offline
|
||||
signed = false
|
||||
}
|
||||
|
||||
if signed {
|
||||
event("beginblock/signing/signed")
|
||||
} else {
|
||||
event("beginblock/signing/missed")
|
||||
}
|
||||
|
||||
pubkey, err := tmtypes.PB2TM.PubKey(mVal.val.PubKey)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
voteInfos[i] = abci.VoteInfo{
|
||||
Validator: abci.Validator{
|
||||
Address: pubkey.Address(),
|
||||
Power: mVal.val.Power,
|
||||
},
|
||||
SignedLastBlock: signed,
|
||||
}
|
||||
}
|
||||
|
||||
// return if no past times
|
||||
if len(pastTimes) <= 0 {
|
||||
return abci.RequestBeginBlock{
|
||||
Header: header,
|
||||
LastCommitInfo: abci.LastCommitInfo{
|
||||
Votes: voteInfos,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Determine capacity before allocation
|
||||
evidence := make([]abci.Evidence, 0)
|
||||
for r.Float64() < params.EvidenceFraction {
|
||||
|
||||
height := header.Height
|
||||
time := header.Time
|
||||
vals := voteInfos
|
||||
|
||||
if r.Float64() < params.PastEvidenceFraction && header.Height > 1 {
|
||||
height = int64(r.Intn(int(header.Height)-1)) + 1 // Tendermint starts at height 1
|
||||
// array indices offset by one
|
||||
time = pastTimes[height-1]
|
||||
vals = pastVoteInfos[height-1]
|
||||
}
|
||||
validator := vals[r.Intn(len(vals))].Validator
|
||||
|
||||
var totalVotingPower int64
|
||||
for _, val := range vals {
|
||||
totalVotingPower += val.Validator.Power
|
||||
}
|
||||
|
||||
evidence = append(evidence,
|
||||
abci.Evidence{
|
||||
Type: tmtypes.ABCIEvidenceTypeDuplicateVote,
|
||||
Validator: validator,
|
||||
Height: height,
|
||||
Time: time,
|
||||
TotalVotingPower: totalVotingPower,
|
||||
},
|
||||
)
|
||||
event("beginblock/evidence")
|
||||
}
|
||||
|
||||
return abci.RequestBeginBlock{
|
||||
Header: header,
|
||||
LastCommitInfo: abci.LastCommitInfo{
|
||||
Votes: voteInfos,
|
||||
},
|
||||
ByzantineValidators: evidence,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
package simulation
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/baseapp"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
)
|
||||
|
||||
// Operation runs a state machine transition, and ensures the transition
|
||||
// happened as expected. The operation could be running and testing a fuzzed
|
||||
// transaction, or doing the same for a message.
|
||||
//
|
||||
// For ease of debugging, an operation returns a descriptive message "action",
|
||||
// which details what this fuzzed state machine transition actually did.
|
||||
//
|
||||
// Operations can optionally provide a list of "FutureOperations" to run later
|
||||
// These will be ran at the beginning of the corresponding block.
|
||||
type Operation func(r *rand.Rand, app *baseapp.BaseApp,
|
||||
ctx sdk.Context, accounts []Account) (
|
||||
OperationMsg OperationMsg, futureOps []FutureOperation, err error)
|
||||
|
||||
// entry kinds for use within OperationEntry
|
||||
const (
|
||||
BeginBlockEntryKind = "begin_block"
|
||||
EndBlockEntryKind = "end_block"
|
||||
MsgEntryKind = "msg"
|
||||
QueuedsgMsgEntryKind = "queued_msg"
|
||||
)
|
||||
|
||||
// OperationEntry - an operation entry for logging (ex. BeginBlock, EndBlock, XxxMsg, etc)
|
||||
type OperationEntry struct {
|
||||
EntryKind string `json:"entry_kind"`
|
||||
Height int64 `json:"height"`
|
||||
Order int64 `json:"order"`
|
||||
Operation json.RawMessage `json:"operation"`
|
||||
}
|
||||
|
||||
// BeginBlockEntry - operation entry for begin block
|
||||
func BeginBlockEntry(height int64) OperationEntry {
|
||||
return OperationEntry{
|
||||
EntryKind: BeginBlockEntryKind,
|
||||
Height: height,
|
||||
Order: -1,
|
||||
Operation: nil,
|
||||
}
|
||||
}
|
||||
|
||||
// EndBlockEntry - operation entry for end block
|
||||
func EndBlockEntry(height int64) OperationEntry {
|
||||
return OperationEntry{
|
||||
EntryKind: EndBlockEntryKind,
|
||||
Height: height,
|
||||
Order: -1,
|
||||
Operation: nil,
|
||||
}
|
||||
}
|
||||
|
||||
// MsgEntry - operation entry for standard msg
|
||||
func MsgEntry(height int64, opMsg OperationMsg, order int64) OperationEntry {
|
||||
return OperationEntry{
|
||||
EntryKind: MsgEntryKind,
|
||||
Height: height,
|
||||
Order: order,
|
||||
Operation: opMsg.MustMarshal(),
|
||||
}
|
||||
}
|
||||
|
||||
// MsgEntry - operation entry for queued msg
|
||||
func QueuedMsgEntry(height int64, opMsg OperationMsg) OperationEntry {
|
||||
return OperationEntry{
|
||||
EntryKind: QueuedsgMsgEntryKind,
|
||||
Height: height,
|
||||
Order: -1,
|
||||
Operation: opMsg.MustMarshal(),
|
||||
}
|
||||
}
|
||||
|
||||
// OperationEntry - log entry text for this operation entry
|
||||
func (oe OperationEntry) MustMarshal() json.RawMessage {
|
||||
out, err := json.Marshal(oe)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
//_____________________________________________________________________
|
||||
|
||||
// OperationMsg - structure for operation output
|
||||
type OperationMsg struct {
|
||||
Route string `json:"route"`
|
||||
Name string `json:"name"`
|
||||
Comment string `json:"comment"`
|
||||
OK bool `json:"ok"`
|
||||
Msg json.RawMessage `json:"msg"`
|
||||
}
|
||||
|
||||
// OperationMsg - create a new operation message from sdk.Msg
|
||||
func NewOperationMsg(msg sdk.Msg, ok bool, comment string) OperationMsg {
|
||||
|
||||
return OperationMsg{
|
||||
Route: msg.Route(),
|
||||
Name: msg.Type(),
|
||||
Comment: comment,
|
||||
OK: ok,
|
||||
Msg: msg.GetSignBytes(),
|
||||
}
|
||||
}
|
||||
|
||||
// OperationMsg - create a new operation message from raw input
|
||||
func NewOperationMsgBasic(route, name, comment string, ok bool, msg []byte) OperationMsg {
|
||||
return OperationMsg{
|
||||
Route: route,
|
||||
Name: name,
|
||||
Comment: comment,
|
||||
OK: ok,
|
||||
Msg: msg,
|
||||
}
|
||||
}
|
||||
|
||||
// NoOpMsg - create a no-operation message
|
||||
func NoOpMsg() OperationMsg {
|
||||
return OperationMsg{
|
||||
Route: "",
|
||||
Name: "no-operation",
|
||||
Comment: "",
|
||||
OK: false,
|
||||
Msg: nil,
|
||||
}
|
||||
}
|
||||
|
||||
// log entry text for this operation msg
|
||||
func (om OperationMsg) String() string {
|
||||
out, err := json.Marshal(om)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return string(out)
|
||||
}
|
||||
|
||||
// Marshal the operation msg, panic on error
|
||||
func (om OperationMsg) MustMarshal() json.RawMessage {
|
||||
out, err := json.Marshal(om)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// add event for event stats
|
||||
func (om OperationMsg) LogEvent(eventLogger func(string)) {
|
||||
pass := "ok"
|
||||
if !om.OK {
|
||||
pass = "failure"
|
||||
}
|
||||
eventLogger(fmt.Sprintf("%v/%v/%v", om.Route, om.Name, pass))
|
||||
}
|
||||
|
||||
// queue of operations
|
||||
type OperationQueue map[int][]Operation
|
||||
|
||||
func newOperationQueue() OperationQueue {
|
||||
operationQueue := make(OperationQueue)
|
||||
return operationQueue
|
||||
}
|
||||
|
||||
// adds all future operations into the operation queue.
|
||||
func queueOperations(queuedOps OperationQueue,
|
||||
queuedTimeOps []FutureOperation, futureOps []FutureOperation) {
|
||||
|
||||
if futureOps == nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, futureOp := range futureOps {
|
||||
if futureOp.BlockHeight != 0 {
|
||||
if val, ok := queuedOps[futureOp.BlockHeight]; ok {
|
||||
queuedOps[futureOp.BlockHeight] = append(val, futureOp.Op)
|
||||
} else {
|
||||
queuedOps[futureOp.BlockHeight] = []Operation{futureOp.Op}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// TODO: Replace with proper sorted data structure, so don't have the
|
||||
// copy entire slice
|
||||
index := sort.Search(
|
||||
len(queuedTimeOps),
|
||||
func(i int) bool {
|
||||
return queuedTimeOps[i].BlockTime.After(futureOp.BlockTime)
|
||||
},
|
||||
)
|
||||
queuedTimeOps = append(queuedTimeOps, FutureOperation{})
|
||||
copy(queuedTimeOps[index+1:], queuedTimeOps[index:])
|
||||
queuedTimeOps[index] = futureOp
|
||||
}
|
||||
}
|
||||
|
||||
//________________________________________________________________________
|
||||
|
||||
// FutureOperation is an operation which will be ran at the beginning of the
|
||||
// provided BlockHeight. If both a BlockHeight and BlockTime are specified, it
|
||||
// will use the BlockHeight. In the (likely) event that multiple operations
|
||||
// are queued at the same block height, they will execute in a FIFO pattern.
|
||||
type FutureOperation struct {
|
||||
BlockHeight int
|
||||
BlockTime time.Time
|
||||
Op Operation
|
||||
}
|
||||
|
||||
//________________________________________________________________________
|
||||
|
||||
// WeightedOperation is an operation with associated weight.
|
||||
// This is used to bias the selection operation within the simulator.
|
||||
type WeightedOperation struct {
|
||||
Weight int
|
||||
Op Operation
|
||||
}
|
||||
|
||||
// WeightedOperations is the group of all weighted operations to simulate.
|
||||
type WeightedOperations []WeightedOperation
|
||||
|
||||
func (ops WeightedOperations) totalWeight() int {
|
||||
totalOpWeight := 0
|
||||
for _, op := range ops {
|
||||
totalOpWeight += op.Weight
|
||||
}
|
||||
return totalOpWeight
|
||||
}
|
||||
|
||||
type selectOpFn func(r *rand.Rand) Operation
|
||||
|
||||
func (ops WeightedOperations) getSelectOpFn() selectOpFn {
|
||||
totalOpWeight := ops.totalWeight()
|
||||
return func(r *rand.Rand) Operation {
|
||||
x := r.Intn(totalOpWeight)
|
||||
for i := 0; i < len(ops); i++ {
|
||||
if x <= ops[i].Weight {
|
||||
return ops[i].Op
|
||||
}
|
||||
x -= ops[i].Weight
|
||||
}
|
||||
// shouldn't happen
|
||||
return ops[0].Op
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package simulation
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
)
|
||||
|
||||
const (
|
||||
// Minimum time per block
|
||||
minTimePerBlock int64 = 10000 / 2
|
||||
|
||||
// Maximum time per block
|
||||
maxTimePerBlock int64 = 10000
|
||||
|
||||
// TODO Remove in favor of binary search for invariant violation
|
||||
onOperation bool = false
|
||||
)
|
||||
|
||||
// TODO explain transitional matrix usage
|
||||
var (
|
||||
// Currently there are 3 different liveness types,
|
||||
// fully online, spotty connection, offline.
|
||||
defaultLivenessTransitionMatrix, _ = CreateTransitionMatrix([][]int{
|
||||
{90, 20, 1},
|
||||
{10, 50, 5},
|
||||
{0, 10, 1000},
|
||||
})
|
||||
|
||||
// 3 states: rand in range [0, 4*provided blocksize],
|
||||
// rand in range [0, 2 * provided blocksize], 0
|
||||
defaultBlockSizeTransitionMatrix, _ = CreateTransitionMatrix([][]int{
|
||||
{85, 5, 0},
|
||||
{15, 92, 1},
|
||||
{0, 3, 99},
|
||||
})
|
||||
)
|
||||
|
||||
// Simulation parameters
|
||||
type Params struct {
|
||||
PastEvidenceFraction float64
|
||||
NumKeys int
|
||||
EvidenceFraction float64
|
||||
InitialLivenessWeightings []int
|
||||
LivenessTransitionMatrix TransitionMatrix
|
||||
BlockSizeTransitionMatrix TransitionMatrix
|
||||
}
|
||||
|
||||
// Return default simulation parameters
|
||||
func DefaultParams() Params {
|
||||
return Params{
|
||||
PastEvidenceFraction: 0.5,
|
||||
NumKeys: 250,
|
||||
EvidenceFraction: 0.5,
|
||||
InitialLivenessWeightings: []int{40, 5, 5},
|
||||
LivenessTransitionMatrix: defaultLivenessTransitionMatrix,
|
||||
BlockSizeTransitionMatrix: defaultBlockSizeTransitionMatrix,
|
||||
}
|
||||
}
|
||||
|
||||
// Return random simulation parameters
|
||||
func RandomParams(r *rand.Rand) Params {
|
||||
return Params{
|
||||
PastEvidenceFraction: r.Float64(),
|
||||
NumKeys: r.Intn(250),
|
||||
EvidenceFraction: r.Float64(),
|
||||
InitialLivenessWeightings: []int{r.Intn(80), r.Intn(10), r.Intn(10)},
|
||||
LivenessTransitionMatrix: defaultLivenessTransitionMatrix,
|
||||
BlockSizeTransitionMatrix: defaultBlockSizeTransitionMatrix,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package simulation
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
"math/rand"
|
||||
"time"
|
||||
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
)
|
||||
|
||||
const (
|
||||
letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
letterIdxBits = 6 // 6 bits to represent a letter index
|
||||
letterIdxMask = 1<<letterIdxBits - 1 // All 1-bits, as many as letterIdxBits
|
||||
letterIdxMax = 63 / letterIdxBits // # of letter indices fitting in 63 bits
|
||||
)
|
||||
|
||||
// shamelessly copied from
|
||||
// https://stackoverflow.com/questions/22892120/how-to-generate-a-random-string-of-a-fixed-length-in-golang#31832326
|
||||
// Generate a random string of a particular length
|
||||
func RandStringOfLength(r *rand.Rand, n int) string {
|
||||
b := make([]byte, n)
|
||||
// A src.Int63() generates 63 random bits, enough for letterIdxMax characters!
|
||||
for i, cache, remain := n-1, r.Int63(), letterIdxMax; i >= 0; {
|
||||
if remain == 0 {
|
||||
cache, remain = r.Int63(), letterIdxMax
|
||||
}
|
||||
if idx := int(cache & letterIdxMask); idx < len(letterBytes) {
|
||||
b[i] = letterBytes[idx]
|
||||
i--
|
||||
}
|
||||
cache >>= letterIdxBits
|
||||
remain--
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
// Generate a random amount
|
||||
// Note: The range of RandomAmount includes max, and is, in fact, biased to return max as well as 0.
|
||||
func RandomAmount(r *rand.Rand, max sdk.Int) sdk.Int {
|
||||
var randInt = big.NewInt(0)
|
||||
switch r.Intn(10) {
|
||||
case 0:
|
||||
// randInt = big.NewInt(0)
|
||||
case 1:
|
||||
randInt = max.BigInt()
|
||||
default: // NOTE: there are 10 total cases.
|
||||
randInt = big.NewInt(0).Rand(r, max.BigInt()) // up to max - 1
|
||||
}
|
||||
return sdk.NewIntFromBigInt(randInt)
|
||||
}
|
||||
|
||||
// RandomDecAmount generates a random decimal amount
|
||||
// Note: The range of RandomDecAmount includes max, and is, in fact, biased to return max as well as 0.
|
||||
func RandomDecAmount(r *rand.Rand, max sdk.Dec) sdk.Dec {
|
||||
var randInt = big.NewInt(0)
|
||||
switch r.Intn(10) {
|
||||
case 0:
|
||||
// randInt = big.NewInt(0)
|
||||
case 1:
|
||||
randInt = max.Int // the underlying big int with all precision bits.
|
||||
default: // NOTE: there are 10 total cases.
|
||||
randInt = big.NewInt(0).Rand(r, max.Int)
|
||||
}
|
||||
return sdk.NewDecFromBigIntWithPrec(randInt, sdk.Precision)
|
||||
}
|
||||
|
||||
// RandTimestamp generates a random timestamp
|
||||
func RandTimestamp(r *rand.Rand) time.Time {
|
||||
// json.Marshal breaks for timestamps greater with year greater than 9999
|
||||
unixTime := r.Int63n(253373529600)
|
||||
return time.Unix(unixTime, 0)
|
||||
}
|
||||
|
||||
// Derive a new rand deterministically from a rand.
|
||||
// Unlike rand.New(rand.NewSource(seed)), the result is "more random"
|
||||
// depending on the source and state of r.
|
||||
// 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)
|
||||
}
|
||||
|
||||
type multiSource []rand.Source
|
||||
|
||||
func (ms multiSource) Int63() (r int64) {
|
||||
for _, source := range ms {
|
||||
r ^= source.Int63()
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func (ms multiSource) Seed(seed int64) {
|
||||
panic("multiSource Seed should not be called")
|
||||
}
|
||||
@@ -0,0 +1,345 @@
|
||||
package simulation
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"os"
|
||||
"os/signal"
|
||||
"runtime/debug"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
abci "github.com/tendermint/tendermint/abci/types"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/baseapp"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
)
|
||||
|
||||
// AppStateFn returns the app state json bytes, the genesis accounts, and the chain identifier
|
||||
type AppStateFn func(r *rand.Rand, accs []Account, genesisTimestamp time.Time) (appState json.RawMessage, accounts []Account, chainId string)
|
||||
|
||||
// Simulate tests application by sending random messages.
|
||||
func Simulate(t *testing.T, app *baseapp.BaseApp,
|
||||
appStateFn AppStateFn, ops WeightedOperations,
|
||||
invariants sdk.Invariants, numBlocks, blockSize int, commit, lean bool) (bool, error) {
|
||||
|
||||
time := time.Now().UnixNano()
|
||||
return SimulateFromSeed(t, app, appStateFn, time, ops,
|
||||
invariants, numBlocks, blockSize, commit, lean)
|
||||
}
|
||||
|
||||
// initialize the chain for the simulation
|
||||
func initChain(
|
||||
r *rand.Rand, params Params, accounts []Account,
|
||||
app *baseapp.BaseApp, appStateFn AppStateFn, genesisTimestamp time.Time,
|
||||
) (mockValidators, []Account) {
|
||||
|
||||
appState, accounts, chainID := appStateFn(r, accounts, genesisTimestamp)
|
||||
|
||||
req := abci.RequestInitChain{
|
||||
AppStateBytes: appState,
|
||||
ChainId: chainID,
|
||||
}
|
||||
res := app.InitChain(req)
|
||||
validators := newMockValidators(r, res.Validators, params)
|
||||
|
||||
return validators, accounts
|
||||
}
|
||||
|
||||
// SimulateFromSeed tests an application by running the provided
|
||||
// operations, testing the provided invariants, but using the provided seed.
|
||||
// TODO split this monster function up
|
||||
func SimulateFromSeed(tb testing.TB, app *baseapp.BaseApp,
|
||||
appStateFn AppStateFn, seed int64, ops WeightedOperations,
|
||||
invariants sdk.Invariants,
|
||||
numBlocks, blockSize int, commit, lean bool) (stopEarly bool, simError error) {
|
||||
|
||||
// in case we have to end early, don't os.Exit so that we can run cleanup code.
|
||||
testingMode, t, b := getTestingMode(tb)
|
||||
fmt.Printf("Starting SimulateFromSeed with randomness "+
|
||||
"created with seed %d\n", int(seed))
|
||||
|
||||
r := rand.New(rand.NewSource(seed))
|
||||
params := RandomParams(r) // := DefaultParams()
|
||||
fmt.Printf("Randomized simulation params: %+v\n", params)
|
||||
|
||||
genesisTimestamp := RandTimestamp(r)
|
||||
fmt.Printf("Starting the simulation from time %v, unixtime %v\n",
|
||||
genesisTimestamp.UTC().Format(time.UnixDate), genesisTimestamp.Unix())
|
||||
|
||||
timeDiff := maxTimePerBlock - minTimePerBlock
|
||||
accs := RandomAccounts(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, accs := initChain(r, params, accs, app, appStateFn, genesisTimestamp)
|
||||
if len(accs) == 0 {
|
||||
return true, fmt.Errorf("must have greater than zero genesis accounts")
|
||||
}
|
||||
|
||||
nextValidators := validators
|
||||
|
||||
header := abci.Header{
|
||||
Height: 1,
|
||||
Time: genesisTimestamp,
|
||||
ProposerAddress: validators.randomProposer(r),
|
||||
}
|
||||
opCount := 0
|
||||
|
||||
// Setup code to catch SIGTERM's
|
||||
c := make(chan os.Signal)
|
||||
signal.Notify(c, os.Interrupt, syscall.SIGTERM, syscall.SIGINT)
|
||||
go func() {
|
||||
receivedSignal := <-c
|
||||
fmt.Printf("\nExiting early due to %s, on block %d, operation %d\n",
|
||||
receivedSignal, header.Height, opCount)
|
||||
simError = fmt.Errorf("Exited due to %s", receivedSignal)
|
||||
stopEarly = true
|
||||
}()
|
||||
|
||||
var pastTimes []time.Time
|
||||
var pastVoteInfos [][]abci.VoteInfo
|
||||
|
||||
request := RandomRequestBeginBlock(r, params,
|
||||
validators, pastTimes, pastVoteInfos, eventStats.tally, header)
|
||||
|
||||
// These are operations which have been queued by previous operations
|
||||
operationQueue := newOperationQueue()
|
||||
timeOperationQueue := []FutureOperation{}
|
||||
|
||||
logWriter := NewLogWriter(testingMode)
|
||||
|
||||
blockSimulator := createBlockSimulator(
|
||||
testingMode, tb, t, params, eventStats.tally, invariants,
|
||||
ops, operationQueue, timeOperationQueue,
|
||||
numBlocks, blockSize, logWriter, lean)
|
||||
|
||||
if !testingMode {
|
||||
b.ResetTimer()
|
||||
} else {
|
||||
// Recover logs in case of panic
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
fmt.Println("Panic with err\n", r)
|
||||
stackTrace := string(debug.Stack())
|
||||
fmt.Println(stackTrace)
|
||||
logWriter.PrintLogs()
|
||||
simError = fmt.Errorf(
|
||||
"Simulation halted due to panic on block %d",
|
||||
header.Height)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// TODO split up the contents of this for loop into new functions
|
||||
for height := 1; height <= numBlocks && !stopEarly; height++ {
|
||||
|
||||
// Log the header time for future lookup
|
||||
pastTimes = append(pastTimes, header.Time)
|
||||
pastVoteInfos = append(pastVoteInfos, request.LastCommitInfo.Votes)
|
||||
|
||||
// Run the BeginBlock handler
|
||||
logWriter.AddEntry(BeginBlockEntry(int64(height)))
|
||||
app.BeginBlock(request)
|
||||
|
||||
if testingMode {
|
||||
assertAllInvariants(t, app, invariants, "BeginBlock", logWriter)
|
||||
}
|
||||
|
||||
ctx := app.NewContext(false, header)
|
||||
|
||||
// Run queued operations. Ignores blocksize if blocksize is too small
|
||||
numQueuedOpsRan := runQueuedOperations(
|
||||
operationQueue, int(header.Height),
|
||||
tb, r, app, ctx, accs, logWriter, eventStats.tally, lean)
|
||||
|
||||
numQueuedTimeOpsRan := runQueuedTimeOperations(
|
||||
timeOperationQueue, int(header.Height), header.Time,
|
||||
tb, r, app, ctx, accs, logWriter, eventStats.tally, lean)
|
||||
|
||||
if testingMode && onOperation {
|
||||
assertAllInvariants(t, app, invariants, "QueuedOperations", logWriter)
|
||||
}
|
||||
|
||||
// run standard operations
|
||||
operations := blockSimulator(r, app, ctx, accs, header)
|
||||
opCount += operations + numQueuedOpsRan + numQueuedTimeOpsRan
|
||||
if testingMode {
|
||||
assertAllInvariants(t, app, invariants, "StandardOperations", logWriter)
|
||||
}
|
||||
|
||||
res := app.EndBlock(abci.RequestEndBlock{})
|
||||
header.Height++
|
||||
header.Time = header.Time.Add(
|
||||
time.Duration(minTimePerBlock) * time.Second)
|
||||
header.Time = header.Time.Add(
|
||||
time.Duration(int64(r.Intn(int(timeDiff)))) * time.Second)
|
||||
header.ProposerAddress = validators.randomProposer(r)
|
||||
logWriter.AddEntry(EndBlockEntry(int64(height)))
|
||||
|
||||
if testingMode {
|
||||
assertAllInvariants(t, app, invariants, "EndBlock", logWriter)
|
||||
}
|
||||
if commit {
|
||||
app.Commit()
|
||||
}
|
||||
|
||||
if header.ProposerAddress == nil {
|
||||
fmt.Printf("\nSimulation stopped early as all validators " +
|
||||
"have been unbonded, there is nobody left propose a block!\n")
|
||||
stopEarly = true
|
||||
break
|
||||
}
|
||||
|
||||
// Generate a random RequestBeginBlock with the current validator set
|
||||
// for the next block
|
||||
request = RandomRequestBeginBlock(r, params, validators,
|
||||
pastTimes, pastVoteInfos, eventStats.tally, header)
|
||||
|
||||
// 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 stopEarly {
|
||||
eventStats.Print()
|
||||
return true, simError
|
||||
}
|
||||
fmt.Printf("\nSimulation complete. Final height (blocks): %d, "+
|
||||
"final time (seconds), : %v, operations ran %d\n",
|
||||
header.Height, header.Time, opCount)
|
||||
|
||||
eventStats.Print()
|
||||
return false, nil
|
||||
}
|
||||
|
||||
//______________________________________________________________________________
|
||||
|
||||
type blockSimFn func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context,
|
||||
accounts []Account, header abci.Header) (opCount int)
|
||||
|
||||
// Returns a function to simulate blocks. Written like this to avoid constant
|
||||
// parameters being passed everytime, to minimize memory overhead.
|
||||
func createBlockSimulator(testingMode bool, tb testing.TB, t *testing.T, params Params,
|
||||
event func(string), invariants sdk.Invariants, ops WeightedOperations,
|
||||
operationQueue OperationQueue, timeOperationQueue []FutureOperation,
|
||||
totalNumBlocks, avgBlockSize int, logWriter LogWriter, lean bool) blockSimFn {
|
||||
|
||||
lastBlocksizeState := 0 // state for [4 * uniform distribution]
|
||||
blocksize := 0
|
||||
selectOp := ops.getSelectOpFn()
|
||||
|
||||
return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context,
|
||||
accounts []Account, header abci.Header) (opCount int) {
|
||||
|
||||
fmt.Printf("\rSimulating... block %d/%d, operation %d/%d. ",
|
||||
header.Height, totalNumBlocks, opCount, blocksize)
|
||||
lastBlocksizeState, blocksize = getBlockSize(r, params, lastBlocksizeState, avgBlockSize)
|
||||
|
||||
type opAndR struct {
|
||||
op 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 i := 0; i < blocksize; i++ {
|
||||
opAndRz = append(opAndRz, opAndR{
|
||||
op: selectOp(r),
|
||||
rand: DeriveRand(r),
|
||||
})
|
||||
}
|
||||
|
||||
for i := 0; i < blocksize; i++ {
|
||||
// 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)
|
||||
opMsg.LogEvent(event)
|
||||
if !lean || opMsg.OK {
|
||||
logWriter.AddEntry(MsgEntry(header.Height, opMsg, int64(i)))
|
||||
}
|
||||
if err != nil {
|
||||
logWriter.PrintLogs()
|
||||
tb.Fatalf("error on operation %d within block %d, %v",
|
||||
header.Height, opCount, err)
|
||||
}
|
||||
|
||||
queueOperations(operationQueue, timeOperationQueue, futureOps)
|
||||
if testingMode {
|
||||
if onOperation {
|
||||
eventStr := fmt.Sprintf("operation: %v", opMsg.String())
|
||||
assertAllInvariants(t, app, invariants, eventStr, logWriter)
|
||||
}
|
||||
if opCount%50 == 0 {
|
||||
fmt.Printf("\rSimulating... block %d/%d, operation %d/%d. ",
|
||||
header.Height, totalNumBlocks, opCount, blocksize)
|
||||
}
|
||||
}
|
||||
opCount++
|
||||
}
|
||||
return opCount
|
||||
}
|
||||
}
|
||||
|
||||
// nolint: errcheck
|
||||
func runQueuedOperations(queueOps map[int][]Operation,
|
||||
height int, tb testing.TB, r *rand.Rand, app *baseapp.BaseApp,
|
||||
ctx sdk.Context, accounts []Account, logWriter LogWriter, tallyEvent func(string), lean bool) (numOpsRan int) {
|
||||
|
||||
queuedOp, ok := queueOps[height]
|
||||
if !ok {
|
||||
return 0
|
||||
}
|
||||
|
||||
numOpsRan = len(queuedOp)
|
||||
for i := 0; i < numOpsRan; i++ {
|
||||
|
||||
// For now, queued operations cannot queue more operations.
|
||||
// If a need arises for us to support queued messages to queue more messages, this can
|
||||
// be changed.
|
||||
opMsg, _, err := queuedOp[i](r, app, ctx, accounts)
|
||||
opMsg.LogEvent(tallyEvent)
|
||||
if !lean || opMsg.OK {
|
||||
logWriter.AddEntry((QueuedMsgEntry(int64(height), opMsg)))
|
||||
}
|
||||
if err != nil {
|
||||
logWriter.PrintLogs()
|
||||
tb.FailNow()
|
||||
}
|
||||
}
|
||||
delete(queueOps, height)
|
||||
return numOpsRan
|
||||
}
|
||||
|
||||
func runQueuedTimeOperations(queueOps []FutureOperation,
|
||||
height int, currentTime time.Time, tb testing.TB, r *rand.Rand,
|
||||
app *baseapp.BaseApp, ctx sdk.Context, accounts []Account,
|
||||
logWriter LogWriter, tallyEvent func(string), lean bool) (numOpsRan int) {
|
||||
|
||||
numOpsRan = 0
|
||||
for len(queueOps) > 0 && currentTime.After(queueOps[0].BlockTime) {
|
||||
|
||||
// For now, queued operations cannot queue more operations.
|
||||
// If a need arises for us to support queued messages to queue more messages, this can
|
||||
// be changed.
|
||||
opMsg, _, err := queueOps[0].Op(r, app, ctx, accounts)
|
||||
opMsg.LogEvent(tallyEvent)
|
||||
if !lean || opMsg.OK {
|
||||
logWriter.AddEntry(QueuedMsgEntry(int64(height), opMsg))
|
||||
}
|
||||
if err != nil {
|
||||
logWriter.PrintLogs()
|
||||
tb.FailNow()
|
||||
}
|
||||
|
||||
queueOps = queueOps[1:]
|
||||
numOpsRan++
|
||||
}
|
||||
return numOpsRan
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package simulation
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
)
|
||||
|
||||
// TransitionMatrix is _almost_ a left stochastic matrix. It is technically
|
||||
// not one due to not normalizing the column values. In the future, if we want
|
||||
// to find the steady state distribution, it will be quite easy to normalize
|
||||
// these values to get a stochastic matrix. Floats aren't currently used as
|
||||
// the default due to non-determinism across architectures
|
||||
type TransitionMatrix struct {
|
||||
weights [][]int
|
||||
// total in each column
|
||||
totals []int
|
||||
n int
|
||||
}
|
||||
|
||||
// CreateTransitionMatrix creates a transition matrix from the provided weights.
|
||||
// TODO: Provide example usage
|
||||
func CreateTransitionMatrix(weights [][]int) (TransitionMatrix, error) {
|
||||
n := len(weights)
|
||||
for i := 0; i < n; i++ {
|
||||
if len(weights[i]) != n {
|
||||
return TransitionMatrix{},
|
||||
fmt.Errorf("Transition Matrix: Non-square matrix provided, error on row %d", i)
|
||||
}
|
||||
}
|
||||
totals := make([]int, n)
|
||||
for row := 0; row < n; row++ {
|
||||
for col := 0; col < n; col++ {
|
||||
totals[col] += weights[row][col]
|
||||
}
|
||||
}
|
||||
return TransitionMatrix{weights, totals, n}, nil
|
||||
}
|
||||
|
||||
// NextState returns the next state randomly chosen using r, and the weightings
|
||||
// provided in the transition matrix.
|
||||
func (t TransitionMatrix) NextState(r *rand.Rand, i int) int {
|
||||
randNum := r.Intn(t.totals[i])
|
||||
for row := 0; row < t.n; row++ {
|
||||
if randNum < t.weights[row][i] {
|
||||
return row
|
||||
}
|
||||
randNum -= t.weights[row][i]
|
||||
}
|
||||
// This line should never get executed
|
||||
return -1
|
||||
}
|
||||
|
||||
// GetMemberOfInitialState takes an initial array of weights, of size n.
|
||||
// It returns a weighted random number in [0,n).
|
||||
func GetMemberOfInitialState(r *rand.Rand, weights []int) int {
|
||||
n := len(weights)
|
||||
total := 0
|
||||
for i := 0; i < n; i++ {
|
||||
total += weights[i]
|
||||
}
|
||||
randNum := r.Intn(total)
|
||||
for state := 0; state < n; state++ {
|
||||
if randNum < weights[state] {
|
||||
return state
|
||||
}
|
||||
randNum -= weights[state]
|
||||
}
|
||||
// This line should never get executed
|
||||
return -1
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package simulation
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"testing"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/baseapp"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
abci "github.com/tendermint/tendermint/abci/types"
|
||||
)
|
||||
|
||||
// assertAll asserts the all invariants against application state
|
||||
func assertAllInvariants(t *testing.T, app *baseapp.BaseApp, invs sdk.Invariants,
|
||||
event string, logWriter LogWriter) {
|
||||
|
||||
ctx := app.NewContext(false, abci.Header{Height: app.LastBlockHeight() + 1})
|
||||
|
||||
for i := 0; i < len(invs); i++ {
|
||||
if err := invs[i](ctx); err != nil {
|
||||
fmt.Printf("Invariants broken after %s\n%s\n", event, err.Error())
|
||||
logWriter.PrintLogs()
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getTestingMode(tb testing.TB) (testingMode bool, t *testing.T, b *testing.B) {
|
||||
testingMode = false
|
||||
if _t, ok := tb.(*testing.T); ok {
|
||||
t = _t
|
||||
testingMode = true
|
||||
} else {
|
||||
b = tb.(*testing.B)
|
||||
}
|
||||
return testingMode, t, b
|
||||
}
|
||||
|
||||
// getBlockSize returns a block size as determined from the transition matrix.
|
||||
// It targets making average block size the provided parameter. The three
|
||||
// states it moves between are:
|
||||
// - "over stuffed" blocks with average size of 2 * avgblocksize,
|
||||
// - normal sized blocks, hitting avgBlocksize on average,
|
||||
// - and empty blocks, with no txs / only txs scheduled from the past.
|
||||
func getBlockSize(r *rand.Rand, params Params,
|
||||
lastBlockSizeState, avgBlockSize int) (state, blocksize int) {
|
||||
|
||||
// TODO: Make default blocksize transition matrix actually make the average
|
||||
// blocksize equal to avgBlockSize.
|
||||
state = params.BlockSizeTransitionMatrix.NextState(r, lastBlockSizeState)
|
||||
switch state {
|
||||
case 0:
|
||||
blocksize = r.Intn(avgBlockSize * 4)
|
||||
case 1:
|
||||
blocksize = r.Intn(avgBlockSize * 2)
|
||||
default:
|
||||
blocksize = 0
|
||||
}
|
||||
return state, blocksize
|
||||
}
|
||||
|
||||
// PeriodicInvariant returns an Invariant function closure that asserts a given
|
||||
// invariant if the mock application's last block modulo the given period is
|
||||
// congruent to the given offset.
|
||||
//
|
||||
// NOTE this function is intended to be used manually used while running
|
||||
// computationally heavy simulations.
|
||||
// TODO reference this function in the codebase probably through use of a switch
|
||||
func PeriodicInvariant(invariant sdk.Invariant, period int, offset int) sdk.Invariant {
|
||||
return func(ctx sdk.Context) error {
|
||||
if int(ctx.BlockHeight())%period == offset {
|
||||
return invariant(ctx)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user