Merge PR #3819: Simulation Refactor

This commit is contained in:
frog power 4000
2019-03-14 19:13:15 +01:00
committed by Christopher Goes
parent 48b6b3884f
commit f0d1efa43c
22 changed files with 582 additions and 528 deletions
+53
View File
@@ -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
}
+25
View File
@@ -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
+30
View File
@@ -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])
}
}
+70
View File
@@ -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() {}
+210
View File
@@ -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,
}
}
+251
View File
@@ -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
}
}
+69
View File
@@ -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,
}
}
+99
View File
@@ -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")
}
+345
View File
@@ -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
}
+70
View File
@@ -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
}
+76
View File
@@ -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
}
}