package simulation import ( "crypto/sha256" "fmt" "math/rand" "sort" "testing" "time" abci "github.com/cometbft/cometbft/abci/types" cmtproto "github.com/cometbft/cometbft/api/cometbft/types/v1" ) // TODO: move this somewhere else const ( TruncatedSize = 20 ) type mockValidator struct { val abci.ValidatorUpdate livenessState int } func (mv mockValidator) String() string { return fmt.Sprintf("mockValidator{%s power:%v state:%v}", string(mv.val.PubKeyBytes), 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("%X", validator.PubKeyBytes) 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) []byte { keys := vals.getKeys() if len(keys) == 0 { return nil } key := keys[r.Intn(len(keys))] proposer := vals[key].val return SumTruncated(proposer.PubKeyBytes) } // updateValidators mimics CometBFT's update logic. func updateValidators( tb testing.TB, r *rand.Rand, params Params, current map[string]mockValidator, updates []abci.ValidatorUpdate, event func(route, op, evResult string), ) map[string]mockValidator { tb.Helper() for _, update := range updates { str := fmt.Sprintf("%X", update.PubKeyBytes) if update.Power == 0 { if _, ok := current[str]; !ok { tb.Fatalf("tried to delete a nonexistent validator: %s", str) } event("end_block", "validator_updates", "kicked") delete(current, str) } else if _, ok := current[str]; ok { // validator already exists event("end_block", "validator_updates", "updated") } else { // Set this new validator current[str] = mockValidator{ update, GetMemberOfInitialState(r, params.InitialLivenessWeightings()), } event("end_block", "validator_updates", "added") } } return current } // RandomRequestFinalizeBlock generates a list of signing validators according to // the provided list of validators, signing fraction, and evidence fraction func RandomRequestFinalizeBlock( r *rand.Rand, params Params, validators mockValidators, pastTimes []time.Time, pastVoteInfos [][]abci.VoteInfo, event func(route, op, evResult string), blockHeight int64, time time.Time, proposer []byte, ) *abci.FinalizeBlockRequest { if len(validators) == 0 { return &abci.FinalizeBlockRequest{ Height: blockHeight, Time: time, ProposerAddress: proposer, } } 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("begin_block", "signing", "signed") } else { event("begin_block", "signing", "missed") } voteInfos[i] = abci.VoteInfo{ Validator: abci.Validator{ Address: SumTruncated(mVal.val.PubKeyBytes), Power: mVal.val.Power, }, BlockIdFlag: cmtproto.BlockIDFlagCommit, } } // return if no past times if len(pastTimes) == 0 { return &abci.FinalizeBlockRequest{ Height: blockHeight, Time: time, ProposerAddress: proposer, DecidedLastCommit: abci.CommitInfo{ Votes: voteInfos, }, } } // TODO: Determine capacity before allocation evidence := make([]abci.Misbehavior, 0) // If the evidenceFraction value is to close to 1.0, // the following loop will most likely never end if params.EvidenceFraction() > 0.9 { // Reduce the evidenceFraction to a more sane value params.evidenceFraction = 0.9 } for r.Float64() < params.EvidenceFraction() { vals := voteInfos height := blockHeight misbehaviorTime := time if r.Float64() < params.PastEvidenceFraction() && height > 1 { height = int64(r.Intn(int(height)-1)) + 1 // CometBFT starts at height 1 // array indices offset by one misbehaviorTime = 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.Misbehavior{ Type: abci.MISBEHAVIOR_TYPE_DUPLICATE_VOTE, Validator: validator, Height: height, Time: misbehaviorTime, TotalVotingPower: totalVotingPower, }, ) event("begin_block", "evidence", "ok") } return &abci.FinalizeBlockRequest{ Height: blockHeight, Time: time, ProposerAddress: proposer, DecidedLastCommit: abci.CommitInfo{ Votes: voteInfos, }, Misbehavior: evidence, } } // SumTruncated returns the first 20 bytes of SHA256 of the bz. func SumTruncated(bz []byte) []byte { hash := sha256.Sum256(bz) return hash[:TruncatedSize] }