diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index 5a8d4e220..da25072ea 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -4,6 +4,9 @@ ### BREAKING CHANGES: +- State + - [state] [\#92](https://github.com/line/tendermint/pull/92) Genesis state + - CLI/RPC/Config - Apps @@ -18,6 +21,7 @@ ### FEATURES: - [rpc] [\#78](https://github.com/line/tendermint/pull/78) Add `Voters` rpc - [consensus] [\#83](https://github.com/line/tendermint/pull/83) Selection voters using random sampling without replacement +- [consensus] [\#92](https://github.com/line/tendermint/pull/92) Apply calculation of voter count ### IMPROVEMENTS: diff --git a/blockchain/v1/reactor_test.go b/blockchain/v1/reactor_test.go index 0bb6abc5e..a402dfb2d 100644 --- a/blockchain/v1/reactor_test.go +++ b/blockchain/v1/reactor_test.go @@ -173,7 +173,6 @@ func (conR *consensusReactorTest) SwitchToConsensus(state sm.State, blocksSynced } func TestFastSyncNoBlockResponse(t *testing.T) { - config = cfg.ResetTestRoot("blockchain_new_reactor_test") defer os.RemoveAll(config.RootDir) genDoc, privVals := randGenesisDoc(1, false, 30) diff --git a/blockchain/v2/reactor_test.go b/blockchain/v2/reactor_test.go index 15ba52c7b..636368b88 100644 --- a/blockchain/v2/reactor_test.go +++ b/blockchain/v2/reactor_test.go @@ -350,7 +350,6 @@ func TestReactorHelperMode(t *testing.T) { var ( channelID = byte(0x40) ) - config := cfg.ResetTestRoot("blockchain_reactor_v2_test") defer os.RemoveAll(config.RootDir) genDoc, privVals := randGenesisDoc(config.ChainID(), 1, false, 30) diff --git a/cmd/tendermint/commands/init.go b/cmd/tendermint/commands/init.go index 1ece45132..cb93d5d3a 100644 --- a/cmd/tendermint/commands/init.go +++ b/cmd/tendermint/commands/init.go @@ -61,6 +61,7 @@ func initFilesWithConfig(config *cfg.Config) error { ChainID: fmt.Sprintf("test-chain-%v", tmrand.Str(6)), GenesisTime: tmtime.Now(), ConsensusParams: types.DefaultConsensusParams(), + VoterParams: types.DefaultVoterParams(), } pubKey, err := pv.GetPubKey() if err != nil { diff --git a/consensus/common_test.go b/consensus/common_test.go index f32ba1032..00f484614 100644 --- a/consensus/common_test.go +++ b/consensus/common_test.go @@ -410,8 +410,12 @@ func loadPrivValidator(config *cfg.Config) *privval.FilePV { } func randState(nValidators int) (*State, []*validatorStub) { + return randStateWithVoterParams(nValidators, types.DefaultVoterParams()) +} + +func randStateWithVoterParams(nValidators int, voterParams *types.VoterParams) (*State, []*validatorStub) { // Get State - state, privVals := randGenesisState(nValidators, false, 10) + state, privVals := randGenesisState(nValidators, false, 10, voterParams) state.LastProofHash = []byte{2} vss := make([]*validatorStub, nValidators) @@ -433,7 +437,7 @@ func theOthers(index int) int { } func forceProposer(cs *State, vals []*validatorStub, index []int, height []int64, round []int) { - for i := 0; i < 1000; i++ { + for i := 0; i < 5000; i++ { allMatch := true firstHash := []byte{byte(i)} currentHash := firstHash @@ -697,7 +701,7 @@ func consensusLogger() log.Logger { func randConsensusNet(nValidators int, testName string, tickerFunc func() TimeoutTicker, appFunc func() abci.Application, configOpts ...func(*cfg.Config)) ([]*State, cleanupFunc) { - genDoc, privVals := randGenesisDoc(nValidators, false, 30) + genDoc, privVals := randGenesisDoc(nValidators, false, 30, types.DefaultVoterParams()) css := make([]*State, nValidators) logger := consensusLogger() configRootDirs := make([]string, 0, nValidators) @@ -735,7 +739,7 @@ func randConsensusNetWithPeers( tickerFunc func() TimeoutTicker, appFunc func(string) abci.Application, ) ([]*State, *types.GenesisDoc, *cfg.Config, cleanupFunc) { - genDoc, privVals := randGenesisDoc(nValidators, false, testMinPower) + genDoc, privVals := randGenesisDoc(nValidators, false, testMinPower, types.DefaultVoterParams()) css := make([]*State, nPeers) logger := consensusLogger() var peer0Config *cfg.Config @@ -797,7 +801,7 @@ func getSwitchIndex(switches []*p2p.Switch, peer p2p.Peer) int { //------------------------------------------------------------------------------- // genesis -func randGenesisDoc(numValidators int, randPower bool, minPower int64) (*types.GenesisDoc, []types.PrivValidator) { +func randGenesisDoc(numValidators int, randPower bool, minPower int64, voterParams *types.VoterParams) (*types.GenesisDoc, []types.PrivValidator) { validators := make([]types.GenesisValidator, numValidators) privValidators := make([]types.PrivValidator, numValidators) for i := 0; i < numValidators; i++ { @@ -814,11 +818,13 @@ func randGenesisDoc(numValidators int, randPower bool, minPower int64) (*types.G GenesisTime: tmtime.Now(), ChainID: config.ChainID(), Validators: validators, + VoterParams: voterParams, }, privValidators } -func randGenesisState(numValidators int, randPower bool, minPower int64) (sm.State, []types.PrivValidator) { - genDoc, privValidators := randGenesisDoc(numValidators, randPower, minPower) +func randGenesisState(numValidators int, randPower bool, minPower int64, voterParams *types.VoterParams) ( + sm.State, []types.PrivValidator) { + genDoc, privValidators := randGenesisDoc(numValidators, randPower, minPower, voterParams) s0, _ := sm.MakeGenesisState(genDoc) return s0, privValidators } diff --git a/consensus/mempool_test.go b/consensus/mempool_test.go index 8e268d444..cbbe995b9 100644 --- a/consensus/mempool_test.go +++ b/consensus/mempool_test.go @@ -27,7 +27,7 @@ func TestMempoolNoProgressUntilTxsAvailable(t *testing.T) { config := ResetConfig("consensus_mempool_txs_available_test") defer os.RemoveAll(config.RootDir) config.Consensus.CreateEmptyBlocks = false - state, privVals := randGenesisState(1, false, 10) + state, privVals := randGenesisState(1, false, 10, nil) cs := newStateWithConfig(config, state, privVals[0], NewCounterApplication()) assertMempool(cs.txNotifier).EnableTxsAvailable() height, round := cs.Height, cs.Round @@ -46,7 +46,7 @@ func TestMempoolProgressAfterCreateEmptyBlocksInterval(t *testing.T) { config := ResetConfig("consensus_mempool_txs_available_test") defer os.RemoveAll(config.RootDir) config.Consensus.CreateEmptyBlocksInterval = ensureTimeout - state, privVals := randGenesisState(1, false, 10) + state, privVals := randGenesisState(1, false, 10, nil) cs := newStateWithConfig(config, state, privVals[0], NewCounterApplication()) assertMempool(cs.txNotifier).EnableTxsAvailable() height, round := cs.Height, cs.Round @@ -62,7 +62,7 @@ func TestMempoolProgressInHigherRound(t *testing.T) { config := ResetConfig("consensus_mempool_txs_available_test") defer os.RemoveAll(config.RootDir) config.Consensus.CreateEmptyBlocks = false - state, privVals := randGenesisState(1, false, 10) + state, privVals := randGenesisState(1, false, 10, nil) cs := newStateWithConfig(config, state, privVals[0], NewCounterApplication()) assertMempool(cs.txNotifier).EnableTxsAvailable() height, round := cs.Height, cs.Round @@ -108,7 +108,7 @@ func deliverTxsRange(cs *State, start, end int) { } func TestMempoolTxConcurrentWithCommit(t *testing.T) { - state, privVals := randGenesisState(1, false, 10) + state, privVals := randGenesisState(1, false, 10, nil) blockDB := dbm.NewMemDB() cs := newStateWithConfigAndBlockStore(config, state, privVals[0], NewCounterApplication(), blockDB) sm.SaveState(blockDB, state) @@ -130,7 +130,7 @@ func TestMempoolTxConcurrentWithCommit(t *testing.T) { } func TestMempoolRmBadTx(t *testing.T) { - state, privVals := randGenesisState(1, false, 10) + state, privVals := randGenesisState(1, false, 10, nil) app := NewCounterApplication() blockDB := dbm.NewMemDB() cs := newStateWithConfigAndBlockStore(config, state, privVals[0], app, blockDB) diff --git a/consensus/reactor_test.go b/consensus/reactor_test.go index 5124238fb..732ca79ee 100644 --- a/consensus/reactor_test.go +++ b/consensus/reactor_test.go @@ -120,7 +120,7 @@ func TestReactorWithEvidence(t *testing.T) { // to unroll unwieldy abstractions. Here we duplicate the code from: // css := randConsensusNet(N, "consensus_reactor_test", newMockTickerFunc(true), newCounter) - genDoc, privVals := randGenesisDoc(nValidators, false, 30) + genDoc, privVals := randGenesisDoc(nValidators, false, 30, nil) css := make([]*State, nValidators) logger := consensusLogger() for i := 0; i < nValidators; i++ { diff --git a/consensus/replay.go b/consensus/replay.go index 3bc20a7ec..9e0d090f8 100644 --- a/consensus/replay.go +++ b/consensus/replay.go @@ -332,7 +332,7 @@ func (h *Handshaker) ReplayBlocks( state.Voters = types.ToVoterAll(state.Validators.Validators) // Should sync it with MakeGenesisState() state.NextValidators = types.NewValidatorSet(vals) - state.NextVoters = types.SelectVoter(state.NextValidators, h.genDoc.Hash()) + state.NextVoters = types.SelectVoter(state.NextValidators, h.genDoc.Hash(), state.VoterParams) } else if len(h.genDoc.Validators) == 0 { // If validator set is not set in genesis and still empty after InitChain, exit. return nil, fmt.Errorf("validator set is nil in genesis and still empty after InitChain") @@ -450,7 +450,7 @@ func (h *Handshaker) replayBlocks( assertAppHashEqualsOneFromBlock(appHash, block) } - appHash, err = sm.ExecCommitBlock(proxyApp.Consensus(), block, h.logger, h.stateDB) + appHash, err = sm.ExecCommitBlock(proxyApp.Consensus(), block, h.logger, h.stateDB, state.VoterParams) if err != nil { return nil, err } diff --git a/consensus/replay_file.go b/consensus/replay_file.go index 8e301e13c..b7d32d2fa 100644 --- a/consensus/replay_file.go +++ b/consensus/replay_file.go @@ -314,8 +314,7 @@ func newConsensusStateForReplay(config cfg.BaseConfig, csConfig *cfg.ConsensusCo mempool, evpool := mock.Mempool{}, sm.MockEvidencePool{} blockExec := sm.NewBlockExecutor(stateDB, log.TestingLogger(), proxyApp.Consensus(), mempool, evpool) - consensusState := NewState(csConfig, state.Copy(), blockExec, - blockStore, mempool, evpool) + consensusState := NewState(csConfig, state.Copy(), blockExec, blockStore, mempool, evpool) consensusState.SetEventBus(eventBus) return consensusState diff --git a/consensus/state.go b/consensus/state.go index 11d9dd871..02d09db01 100644 --- a/consensus/state.go +++ b/consensus/state.go @@ -936,12 +936,7 @@ func (cs *State) enterPropose(height int64, round int) { } address := pubKey.Address() - // if not a validator, we're done - if !cs.Voters.HasAddress(address) { - logger.Debug("This node is not a validator", "addr", address, "vals", cs.Voters) - return - } - + // I'm a proposer, but I might not be a voter if cs.isProposer(address) { logger.Info("enterPropose: Our turn to propose", "proposer", @@ -955,6 +950,13 @@ func (cs *State) enterPropose(height int64, round int) { cs.Proposer.Address, "privValidator", cs.privValidator) + + } + + if !cs.Voters.HasAddress(address) { + logger.Debug("This node is not elected as a voter") + } else { + logger.Debug("This node is elected as a voter") } } @@ -1462,9 +1464,7 @@ func (cs *State) finalizeCommit(height int64) { var err error var retainHeight int64 stateCopy, retainHeight, err = cs.blockExec.ApplyBlock( - stateCopy, - types.BlockID{Hash: block.Hash(), PartsHeader: blockParts.Header()}, - block) + stateCopy, types.BlockID{Hash: block.Hash(), PartsHeader: blockParts.Header()}, block) if err != nil { cs.Logger.Error("Error on ApplyBlock. Did the application crash? Please restart tendermint", "err", err) err := tmos.Kill() diff --git a/consensus/state_test.go b/consensus/state_test.go index 636bc4330..58ebba323 100644 --- a/consensus/state_test.go +++ b/consensus/state_test.go @@ -11,6 +11,7 @@ import ( "github.com/stretchr/testify/require" cstypes "github.com/tendermint/tendermint/consensus/types" + "github.com/tendermint/tendermint/crypto" "github.com/tendermint/tendermint/libs/log" tmpubsub "github.com/tendermint/tendermint/libs/pubsub" tmrand "github.com/tendermint/tendermint/libs/rand" @@ -1845,3 +1846,154 @@ func subscribeUnBuffered(eventBus *types.EventBus, q tmpubsub.Query) <-chan tmpu } return sub.Out() } + +func makeVssMap(vss []*validatorStub) map[crypto.PubKey]*validatorStub { + vssMap := make(map[crypto.PubKey]*validatorStub) + for _, pv := range vss { + pubKey, _ := pv.GetPubKey() + vssMap[pubKey] = pv + } + return vssMap +} + +func votersPrivVals(voterSet *types.VoterSet, vssMap map[crypto.PubKey]*validatorStub) []*validatorStub { + totalVotingPower := voterSet.TotalVotingPower() + votingPower := int64(0) + voters := 0 + for i, v := range voterSet.Voters { + vssMap[v.PubKey].Index = i // NOTE: re-indexing for new voters + if votingPower < totalVotingPower*2/3+1 { + votingPower += v.VotingPower + voters++ + } + } + result := make([]*validatorStub, voters) + for i := 0; i < voters; i++ { + result[i] = vssMap[voterSet.Voters[i].PubKey] + } + return result +} + +func createProposalBlockByOther(cs *State, other *validatorStub, round int) ( + block *types.Block, blockParts *types.PartSet) { + var commit *types.Commit + switch { + case cs.Height == 1: + commit = types.NewCommit(0, 0, types.BlockID{}, nil) + case cs.LastCommit.HasTwoThirdsMajority(): + commit = cs.LastCommit.MakeCommit() + default: + return + } + + pubKey, err := other.GetPubKey() + if err != nil { + return + } + proposerAddr := pubKey.Address() + message := cs.state.MakeHashMessage(round) + + proof, err := other.GenerateVRFProof(message) + if err != nil { + return + } + return cs.blockExec.CreateProposalBlock(cs.Height, cs.state, commit, proposerAddr, round, proof) +} + +func proposeBlock(t *testing.T, cs *State, round int, vssMap map[crypto.PubKey]*validatorStub) types.BlockID { + newBlock, blockParts := createProposalBlockByOther(cs, vssMap[cs.Proposer.PubKey], round) + proposal := types.NewProposal(cs.Height, round, -1, types.BlockID{ + Hash: newBlock.Hash(), PartsHeader: blockParts.Header()}) + if err := vssMap[cs.Proposer.PubKey].SignProposal(config.ChainID(), proposal); err != nil { + t.Fatal("failed to sign bad proposal", err) + } + + // set the proposal block + if err := cs.SetProposalAndBlock(proposal, newBlock, blockParts, "some peer"); err != nil { + t.Fatal(err) + } + return types.BlockID{Hash: newBlock.Hash(), PartsHeader: blockParts.Header()} +} + +func TestStateFullRoundWithSelectedVoter(t *testing.T) { + cs, vss := randStateWithVoterParams(10, &types.VoterParams{ + VoterElectionThreshold: 5, + MaxTolerableByzantinePercentage: 20, + ElectionPrecision: 2}) + vss[0].Height = 1 // this is needed because of `incrementHeight(vss[1:]...)` of randStateWithVoterParams() + vssMap := makeVssMap(vss) + height, round := cs.Height, cs.Round + + voteCh := subscribeUnBuffered(cs.eventBus, types.EventQueryVote) + propCh := subscribe(cs.eventBus, types.EventQueryCompleteProposal) + newRoundCh := subscribe(cs.eventBus, types.EventQueryNewRound) + newBlockCh := subscribe(cs.eventBus, types.EventQueryNewBlock) + + startTestRound(cs, height, round) + + // height 1 + ensureNewRound(newRoundCh, height, round) + privPubKey, _ := cs.privValidator.GetPubKey() + if !cs.isProposer(privPubKey.Address()) { + blockID := proposeBlock(t, cs, round, vssMap) + ensureProposal(propCh, height, round, blockID) + } else { + ensureNewProposal(propCh, height, round) + } + + propBlock := cs.GetRoundState().ProposalBlock + voters := cs.Voters + voterPrivVals := votersPrivVals(voters, vssMap) + signAddVotes(cs, types.PrevoteType, propBlock.Hash(), propBlock.MakePartSet(types.BlockPartSizeBytes).Header(), + voterPrivVals...) + + for range voterPrivVals { + ensurePrevote(voteCh, height, round) // wait for prevote + } + + signAddVotes(cs, types.PrecommitType, propBlock.Hash(), propBlock.MakePartSet(types.BlockPartSizeBytes).Header(), + voterPrivVals...) + + for range voterPrivVals { + ensurePrecommit(voteCh, height, round) // wait for precommit + } + + ensureNewBlock(newBlockCh, height) + + // height 2 + incrementHeight(vss...) + + ensureNewRound(newRoundCh, height+1, 0) + + height = cs.Height + privPubKey, _ = cs.privValidator.GetPubKey() + if !cs.isProposer(privPubKey.Address()) { + blockID := proposeBlock(t, cs, round, vssMap) + ensureProposal(propCh, height, round, blockID) + } else { + ensureNewProposal(propCh, height, round) + } + + propBlock = cs.GetRoundState().ProposalBlock + voters = cs.Voters + voterPrivVals = votersPrivVals(voters, vssMap) + + signAddVotes(cs, types.PrevoteType, propBlock.Hash(), propBlock.MakePartSet(types.BlockPartSizeBytes).Header(), + voterPrivVals...) + + for range voterPrivVals { + ensurePrevote(voteCh, height, round) // wait for prevote + } + + signAddVotes(cs, types.PrecommitType, propBlock.Hash(), propBlock.MakePartSet(types.BlockPartSizeBytes).Header(), + voterPrivVals...) + + for range voterPrivVals { + ensurePrecommit(voteCh, height, round) // wait for precommit + } + + ensureNewBlock(newBlockCh, height) + + // we're going to roll right into new height + ensureNewRound(newRoundCh, height+1, 0) +} diff --git a/evidence/pool.go b/evidence/pool.go index e7310e7a9..75ac7f900 100644 --- a/evidence/pool.go +++ b/evidence/pool.go @@ -108,7 +108,7 @@ func (evpool *Pool) AddEvidence(evidence types.Evidence) error { // fetch the validator and return its voting power as its priority // TODO: something better ? - valSet, _, err := sm.LoadValidators(evpool.stateDB, evidence.Height()) + valSet, err := sm.LoadValidators(evpool.stateDB, evidence.Height()) if err != nil { return err } diff --git a/go.mod b/go.mod index d40b184c5..aadb452fd 100644 --- a/go.mod +++ b/go.mod @@ -33,9 +33,7 @@ require ( github.com/tendermint/tm-db v0.5.1 github.com/yahoo/coname v0.0.0-20170609175141-84592ddf8673 // indirect golang.org/x/crypto v0.0.0-20200406173513-056763e48d71 - golang.org/x/mod v0.3.0 // indirect golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e - golang.org/x/tools v0.0.0-20200519205726-57a9e4404bf7 // indirect google.golang.org/grpc v1.28.1 gopkg.in/yaml.v3 v3.0.0-20200506231410-2ff61e1afc86 ) diff --git a/rpc/core/consensus.go b/rpc/core/consensus.go index d98385520..a28de0358 100644 --- a/rpc/core/consensus.go +++ b/rpc/core/consensus.go @@ -18,11 +18,12 @@ import ( // // More: https://docs.tendermint.com/master/rpc/#/Info/validators func Voters(ctx *rpctypes.Context, heightPtr *int64, page, perPage int) (*ctypes.ResultVoters, error) { - return voters(ctx, heightPtr, page, perPage, sm.LoadValidators) + return voters(ctx, heightPtr, page, perPage, sm.LoadVoters) } func voters(ctx *rpctypes.Context, heightPtr *int64, page, perPage int, - loadFunc func(db dbm.DB, height int64) (*types.ValidatorSet, *types.VoterSet, error)) (*ctypes.ResultVoters, error) { + loadFunc func(db dbm.DB, height int64, voterParams *types.VoterParams) (*types.VoterSet, error)) ( + *ctypes.ResultVoters, error) { // The latest validator that we know is the // NextValidator of the last block. height := consensusState.GetState().LastBlockHeight + 1 @@ -31,7 +32,7 @@ func voters(ctx *rpctypes.Context, heightPtr *int64, page, perPage int, return nil, err } - _, voters, err := loadFunc(stateDB, height) + voters, err := loadFunc(stateDB, height, consensusState.GetState().VoterParams) if err != nil { return nil, err } diff --git a/rpc/core/status.go b/rpc/core/status.go index 815a35ab8..b745f61e1 100644 --- a/rpc/core/status.go +++ b/rpc/core/status.go @@ -96,7 +96,7 @@ func validatorAtHeight(h int64) *types.Validator { // If we've moved to the next height, retrieve the validator set from DB. if lastBlockHeight > h { // ValidatorOrVoter: validator - vals, _, err := sm.LoadValidators(stateDB, h) + vals, err := sm.LoadValidators(stateDB, h) if err != nil { return nil // should not happen } diff --git a/state/execution.go b/state/execution.go index 618eb4439..df2f13632 100644 --- a/state/execution.go +++ b/state/execution.go @@ -137,7 +137,8 @@ func (blockExec *BlockExecutor) ApplyBlock( } startTime := time.Now().UnixNano() - abciResponses, err := execBlockOnProxyApp(blockExec.logger, blockExec.proxyApp, block, blockExec.db) + abciResponses, err := execBlockOnProxyApp(blockExec.logger, blockExec.proxyApp, block, blockExec.db, + state.VoterParams) endTime := time.Now().UnixNano() blockExec.metrics.BlockProcessingTime.Observe(float64(endTime-startTime) / 1000000) if err != nil { @@ -257,6 +258,7 @@ func execBlockOnProxyApp( proxyAppConn proxy.AppConnConsensus, block *types.Block, stateDB dbm.DB, + voterParams *types.VoterParams, ) (*ABCIResponses, error) { var validTxs, invalidTxs = 0, 0 @@ -282,7 +284,7 @@ func execBlockOnProxyApp( } proxyAppConn.SetResponseCallback(proxyCb) - commitInfo, byzVals := getBeginBlockValidatorInfo(block, stateDB) + commitInfo, byzVals := getBeginBlockValidatorInfo(block, stateDB, voterParams) // Begin block var err error @@ -317,13 +319,14 @@ func execBlockOnProxyApp( return abciResponses, nil } -func getBeginBlockValidatorInfo(block *types.Block, stateDB dbm.DB) (abci.LastCommitInfo, []abci.Evidence) { +func getBeginBlockValidatorInfo(block *types.Block, stateDB dbm.DB, voterParams *types.VoterParams) ( + abci.LastCommitInfo, []abci.Evidence) { voteInfos := make([]abci.VoteInfo, block.LastCommit.Size()) // block.Height=1 -> LastCommitInfo.Votes are empty. // Remember that the first LastCommit is intentionally empty, so it makes // sense for LastCommitInfo.Votes to also be empty. if block.Height > 1 { - _, lastVoterSet, err := LoadValidators(stateDB, block.Height-1) + lastVoterSet, err := LoadVoters(stateDB, block.Height-1, voterParams) if err != nil { panic(err) } @@ -354,7 +357,7 @@ func getBeginBlockValidatorInfo(block *types.Block, stateDB dbm.DB) (abci.LastCo // We need the validator set. We already did this in validateBlock. // TODO: Should we instead cache the valset in the evidence itself and add // `SetValidatorSet()` and `ToABCI` methods ? - _, voterSet, err := LoadValidators(stateDB, ev.Height()) + voterSet, err := LoadVoters(stateDB, ev.Height(), voterParams) if err != nil { panic(err) } @@ -438,13 +441,14 @@ func updateState( return state, fmt.Errorf("error get proof of hash: %v", err) } - nextVoters := types.SelectVoter(nValSet, proofHash) + nextVoters := types.SelectVoter(nValSet, proofHash, state.VoterParams) // NOTE: the AppHash has not been populated. // It will be filled on state.Save. return State{ Version: nextVersion, ChainID: state.ChainID, + VoterParams: state.VoterParams, LastBlockHeight: header.Height, LastBlockID: blockID, LastBlockTime: header.Time, @@ -509,8 +513,9 @@ func ExecCommitBlock( block *types.Block, logger log.Logger, stateDB dbm.DB, + voterParams *types.VoterParams, ) ([]byte, error) { - _, err := execBlockOnProxyApp(logger, appConnConsensus, block, stateDB) + _, err := execBlockOnProxyApp(logger, appConnConsensus, block, stateDB, voterParams) if err != nil { logger.Error("Error executing block on proxy app", "height", block.Height, "err", err) return nil, err diff --git a/state/execution_test.go b/state/execution_test.go index 15c478ad0..8676f08ca 100644 --- a/state/execution_test.go +++ b/state/execution_test.go @@ -98,7 +98,7 @@ func TestBeginBlockValidators(t *testing.T) { // block for height 2 block, _ := state.MakeBlock(2, makeTxs(2), lastCommit, nil, proposer.Address, 0, proof) - _, err = sm.ExecCommitBlock(proxyApp.Consensus(), block, log.TestingLogger(), stateDB) + _, err = sm.ExecCommitBlock(proxyApp.Consensus(), block, log.TestingLogger(), stateDB, state.VoterParams) require.Nil(t, err, tc.desc) // -> app receives a list of validators with a bool indicating if they signed @@ -169,7 +169,7 @@ func TestBeginBlockByzantineValidators(t *testing.T) { block, _ := state.MakeBlock(10, makeTxs(2), lastCommit, nil, proposer.Address, 0, proof) block.Time = now block.Evidence.Evidence = tc.evidence - _, err = sm.ExecCommitBlock(proxyApp.Consensus(), block, log.TestingLogger(), stateDB) + _, err = sm.ExecCommitBlock(proxyApp.Consensus(), block, log.TestingLogger(), stateDB, state.VoterParams) require.Nil(t, err, tc.desc) // -> app must receive an index of the byzantine validator diff --git a/state/state.go b/state/state.go index b721ba092..ebcebae86 100644 --- a/state/state.go +++ b/state/state.go @@ -12,8 +12,8 @@ import ( "github.com/tendermint/tendermint/version" ) -// database keys var ( + // database keys stateKey = []byte("stateKey") ) @@ -53,7 +53,8 @@ type State struct { Version Version // immutable - ChainID string + ChainID string + VoterParams *types.VoterParams // LastBlockHeight=0 at genesis (ie. block(H=0) does not exist) LastBlockHeight int64 @@ -95,8 +96,9 @@ func (state State) MakeHashMessage(round int) []byte { // Copy makes a copy of the State for mutating. func (state State) Copy() State { return State{ - Version: state.Version, - ChainID: state.ChainID, + Version: state.Version, + ChainID: state.ChainID, + VoterParams: state.VoterParams, LastBlockHeight: state.LastBlockHeight, LastBlockID: state.LastBlockID, @@ -249,8 +251,9 @@ func MakeGenesisState(genDoc *types.GenesisDoc) (State, error) { } return State{ - Version: initStateVersion, - ChainID: genDoc.ChainID, + Version: initStateVersion, + ChainID: genDoc.ChainID, + VoterParams: genDoc.VoterParams, LastBlockHeight: 0, LastBlockID: types.BlockID{}, @@ -260,7 +263,7 @@ func MakeGenesisState(genDoc *types.GenesisDoc) (State, error) { LastProofHash: genDoc.Hash(), NextValidators: nextValidatorSet, - NextVoters: types.SelectVoter(nextValidatorSet, genDoc.Hash()), + NextVoters: types.SelectVoter(nextValidatorSet, genDoc.Hash(), genDoc.VoterParams), Validators: validatorSet, Voters: types.ToVoterAll(validatorSet.Validators), LastVoters: &types.VoterSet{}, diff --git a/state/state_test.go b/state/state_test.go index 489ebc932..b985893d1 100644 --- a/state/state_test.go +++ b/state/state_test.go @@ -185,16 +185,16 @@ func TestValidatorSimpleSaveLoad(t *testing.T) { assert := assert.New(t) // Can't load anything for height 0. - _, _, err := sm.LoadValidators(stateDB, 0) + _, err := sm.LoadVoters(stateDB, 0, state.VoterParams) assert.IsType(sm.ErrNoValSetForHeight{}, err, "expected err at height 0") // Should be able to load for height 1. - _, v, err := sm.LoadValidators(stateDB, 1) + v, err := sm.LoadVoters(stateDB, 1, state.VoterParams) assert.Nil(err, "expected no err at height 1") assert.Equal(v.Hash(), state.Validators.Hash(), "expected validator hashes to match") // Should be able to load for height 2. - _, v, err = sm.LoadValidators(stateDB, 2) + v, err = sm.LoadVoters(stateDB, 2, state.VoterParams) assert.Nil(err, "expected no err at height 2") assert.Equal(v.Hash(), state.NextValidators.Hash(), "expected validator hashes to match") @@ -203,9 +203,9 @@ func TestValidatorSimpleSaveLoad(t *testing.T) { nextHeight := state.LastBlockHeight + 1 sm.SaveValidatorsInfo(stateDB, nextHeight+1, state.LastHeightValidatorsChanged, state.LastProofHash, state.NextValidators) - _, vp0, err := sm.LoadValidators(stateDB, nextHeight+0) + vp0, err := sm.LoadVoters(stateDB, nextHeight+0, state.VoterParams) assert.Nil(err, "expected no err") - _, vp1, err := sm.LoadValidators(stateDB, nextHeight+1) + vp1, err := sm.LoadVoters(stateDB, nextHeight+1, state.VoterParams) assert.Nil(err, "expected no err") assert.Equal(vp0.Hash(), state.Validators.Hash(), "expected validator hashes to match") assert.Equal(vp1.Hash(), state.NextValidators.Hash(), "expected next validator hashes to match") @@ -259,7 +259,7 @@ func TestOneValidatorChangesSaveLoad(t *testing.T) { } for i, power := range testCases { - _, v, err := sm.LoadValidators(stateDB, int64(i+1+1)) // +1 because vset changes delayed by 1 block. + v, err := sm.LoadVoters(stateDB, int64(i+1+1), state.VoterParams) // +1 because vset changes delayed by 1 block. assert.Nil(t, err, fmt.Sprintf("expected no err at height %d", i)) assert.Equal(t, v.Size(), 1, "validator set size is greater than 1: %d", v.Size()) _, val := v.GetByIndex(0) @@ -863,11 +863,11 @@ func TestStoreLoadValidatorsIncrementsProposerPriority(t *testing.T) { nextHeight := state.LastBlockHeight + 1 - v0, _, err := sm.LoadValidators(stateDB, nextHeight) + v0, err := sm.LoadValidators(stateDB, nextHeight) assert.Nil(t, err) acc0 := v0.Validators[0].ProposerPriority - v1, _, err := sm.LoadValidators(stateDB, nextHeight+1) + v1, err := sm.LoadValidators(stateDB, nextHeight+1) assert.Nil(t, err) acc1 := v1.Validators[0].ProposerPriority @@ -906,7 +906,7 @@ func TestManyValidatorChangesSaveLoad(t *testing.T) { state.LastProofHash, state.NextValidators) // Load nextheight, it should be the oldpubkey. - v0, _, err := sm.LoadValidators(stateDB, nextHeight) + v0, err := sm.LoadValidators(stateDB, nextHeight) assert.Nil(t, err) assert.Equal(t, valSetSize, v0.Size()) index, val := v0.GetByAddress(pubkeyOld.Address()) @@ -916,7 +916,7 @@ func TestManyValidatorChangesSaveLoad(t *testing.T) { } // Load nextheight+1, it should be the new pubkey. - v1, _, err := sm.LoadValidators(stateDB, nextHeight+1) + v1, err := sm.LoadValidators(stateDB, nextHeight+1) assert.Nil(t, err) assert.Equal(t, valSetSize, v1.Size()) index, val = v1.GetByAddress(pubkey.Address()) diff --git a/state/store.go b/state/store.go index 89dfbf8ce..f9f75207a 100644 --- a/state/store.go +++ b/state/store.go @@ -173,7 +173,7 @@ func PruneStates(db dbm.DB, from int64, to int64) error { if keepVals[h] { v := loadValidatorsInfo(db, calcValidatorsKey(h)) if v.ValidatorSet == nil { - v.ValidatorSet, _, err = LoadValidators(db, h) + v.ValidatorSet, err = LoadValidators(db, h) if err != nil { return err } @@ -291,12 +291,37 @@ func (valInfo *ValidatorsInfo) Bytes() []byte { return cdc.MustMarshalBinaryBare(valInfo) } -// LoadValidators loads the VoterSet for a given height. +// LoadValidators loads the ValidatorSet for a given height. // Returns ErrNoValSetForHeight if the validator set can't be found for this height. -func LoadValidators(db dbm.DB, height int64) (*types.ValidatorSet, *types.VoterSet, error) { +func LoadValidators(db dbm.DB, height int64) (*types.ValidatorSet, error) { valInfo := loadValidatorsInfo(db, calcValidatorsKey(height)) if valInfo == nil { - return nil, nil, ErrNoValSetForHeight{height} + return nil, ErrNoValSetForHeight{height} + } + if valInfo.ValidatorSet == nil { + lastStoredHeight := lastStoredHeightFor(height, valInfo.LastHeightChanged) + valInfo2 := loadValidatorsInfo(db, calcValidatorsKey(lastStoredHeight)) + if valInfo2 == nil || valInfo2.ValidatorSet == nil { + panic( + fmt.Sprintf("Couldn't find validators at height %d (height %d was originally requested)", + lastStoredHeight, + height, + ), + ) + } + valInfo2.ValidatorSet.IncrementProposerPriority(int(height - lastStoredHeight)) // mutate + valInfo = valInfo2 + } + + return valInfo.ValidatorSet, nil +} + +// LoadVoters loads the VoterSet for a given height. +// Returns ErrNoValSetForHeight if the validator set can't be found for this height. +func LoadVoters(db dbm.DB, height int64, voterParams *types.VoterParams) (*types.VoterSet, error) { + valInfo := loadValidatorsInfo(db, calcValidatorsKey(height)) + if valInfo == nil { + return nil, ErrNoValSetForHeight{height} } if valInfo.ValidatorSet == nil { proofHash := valInfo.ProofHash // store proof hash of the height @@ -315,7 +340,7 @@ func LoadValidators(db dbm.DB, height int64) (*types.ValidatorSet, *types.VoterS valInfo.ProofHash = proofHash // reload proof again } - return valInfo.ValidatorSet, types.SelectVoter(valInfo.ValidatorSet, valInfo.ProofHash), nil + return types.SelectVoter(valInfo.ValidatorSet, valInfo.ProofHash, voterParams), nil } func lastStoredHeightFor(height, lastHeightChanged int64) int64 { diff --git a/state/store_test.go b/state/store_test.go index f0cf58e7f..eef21c068 100644 --- a/state/store_test.go +++ b/state/store_test.go @@ -23,7 +23,7 @@ func TestStoreLoadValidators(t *testing.T) { // 1) LoadValidators loads validators using a height where they were last changed sm.SaveValidatorsInfo(stateDB, 1, 1, []byte{}, vals) sm.SaveValidatorsInfo(stateDB, 2, 1, []byte{}, vals) - loadedVals, _, err := sm.LoadValidators(stateDB, 2) + loadedVals, err := sm.LoadValidators(stateDB, 2) require.NoError(t, err) assert.NotZero(t, loadedVals.Size()) @@ -31,7 +31,7 @@ func TestStoreLoadValidators(t *testing.T) { sm.SaveValidatorsInfo(stateDB, sm.ValSetCheckpointInterval, 1, []byte{}, vals) - loadedVals, _, err = sm.LoadValidators(stateDB, sm.ValSetCheckpointInterval) + loadedVals, err = sm.LoadValidators(stateDB, sm.ValSetCheckpointInterval) require.NoError(t, err) assert.NotZero(t, loadedVals.Size()) } @@ -59,7 +59,7 @@ func BenchmarkLoadValidators(b *testing.B) { b.Run(fmt.Sprintf("height=%d", i), func(b *testing.B) { for n := 0; n < b.N; n++ { - _, _, err := sm.LoadValidators(stateDB, int64(i)) + _, err := sm.LoadValidators(stateDB, int64(i)) if err != nil { b.Fatal(err) } @@ -145,7 +145,7 @@ func TestPruneStates(t *testing.T) { expectABCI := sliceToMap(tc.expectABCI) for h := int64(1); h <= tc.makeHeights; h++ { - vals, _, err := sm.LoadValidators(db, h) + vals, err := sm.LoadValidators(db, h) if expectVals[h] { require.NoError(t, err, "validators height %v", h) require.NotNil(t, vals) diff --git a/state/validation.go b/state/validation.go index 9b109d946..5b91f8885 100644 --- a/state/validation.go +++ b/state/validation.go @@ -208,7 +208,7 @@ func VerifyEvidence(stateDB dbm.DB, state State, evidence types.Evidence) error ) } - _, voterSet, err := LoadValidators(stateDB, evidence.Height()) + voterSet, err := LoadVoters(stateDB, evidence.Height(), state.VoterParams) if err != nil { // TODO: if err is just that we cant find it cuz we pruned, ignore. // TODO: if its actually bad evidence, punish peer diff --git a/types/genesis.go b/types/genesis.go index 7ca328c9a..24ec17c48 100644 --- a/types/genesis.go +++ b/types/genesis.go @@ -34,12 +34,22 @@ type GenesisValidator struct { Name string `json:"name"` } +type VoterParams struct { + VoterElectionThreshold int `json:"voter_election_threshold"` + MaxTolerableByzantinePercentage int `json:"max_tolerable_byzantine_percentage"` + + // As a unit of precision, if it is 1, it is 0.9, and if it is 2, it is 0.99. + // The default is 5, with a precision of 0.99999. + ElectionPrecision int `json:"election_precision"` +} + // GenesisDoc defines the initial conditions for a tendermint blockchain, in particular its validator set. type GenesisDoc struct { GenesisTime time.Time `json:"genesis_time"` ChainID string `json:"chain_id"` ConsensusParams *ConsensusParams `json:"consensus_params,omitempty"` Validators []GenesisValidator `json:"validators,omitempty"` + VoterParams *VoterParams `json:"voter_params,omitempty"` AppHash tmbytes.HexBytes `json:"app_hash"` AppState json.RawMessage `json:"app_state,omitempty"` } @@ -79,6 +89,12 @@ func (genDoc *GenesisDoc) ValidateAndComplete() error { return err } + if genDoc.VoterParams == nil { + genDoc.VoterParams = DefaultVoterParams() + } else if err := genDoc.VoterParams.Validate(); err != nil { + return err + } + for i, v := range genDoc.Validators { if v.Power == 0 { return errors.Errorf("the genesis file cannot contain validators with no voting power: %v", v) diff --git a/types/genesis_test.go b/types/genesis_test.go index ee713a6e7..16961ea7b 100644 --- a/types/genesis_test.go +++ b/types/genesis_test.go @@ -47,6 +47,15 @@ func TestGenesisBad(t *testing.T) { `},"power":"10","name":""}` + `]}`, ), + // missing some params in voter_params + []byte( + `{"chain_id":"mychain", "validators":[` + + `{"pub_key":{` + + `"type":"tendermint/PubKeyEd25519","value":"AT/+aaL1eB0477Mud9JMm8Sh8BIvOYlPGC9KkIUmFaE="` + + `},"power":"10","name":""}], ` + + `"voter_params":{"voter_election_threshold":"1"}` + + `}`, + ), } for _, testCase := range testCases { @@ -62,7 +71,7 @@ func TestGenesisGood(t *testing.T) { `{"pub_key":{` + `"type":"tendermint/PubKeyEd25519","value":"AT/+aaL1eB0477Mud9JMm8Sh8BIvOYlPGC9KkIUmFaE="` + `},"power":"10","name":""}` + - `],"app_hash":"","app_state":{"account_owner": "Bob"}}`, + `],"voter_params":null, "app_hash":"","app_state":{"account_owner": "Bob"}}`, ) _, err := GenesisDocFromJSON(genDocBytes) assert.NoError(t, err, "expected no error for good genDoc json") diff --git a/types/params.go b/types/params.go index 538bbbd6d..1ac1aa34e 100644 --- a/types/params.go +++ b/types/params.go @@ -19,6 +19,10 @@ const ( // MaxBlockPartsCount is the maximum number of block parts. MaxBlockPartsCount = (MaxBlockSizeBytes / BlockPartSizeBytes) + 1 + + DefaultVoterElectionThreshold = 33 + DefaultMaxTolerableByzantinePercentage = 20 + DefaultElectionPrecision = 5 // 5 is 0.99999 ) // ConsensusParams contains consensus critical parameters that determine the @@ -68,6 +72,29 @@ func DefaultConsensusParams() *ConsensusParams { } } +// DefaultVoterParams returns a default VoterParams. +func DefaultVoterParams() *VoterParams { + return &VoterParams{ + VoterElectionThreshold: DefaultVoterElectionThreshold, + MaxTolerableByzantinePercentage: DefaultMaxTolerableByzantinePercentage, + ElectionPrecision: DefaultElectionPrecision} +} + +func (params *VoterParams) Validate() error { + if params.VoterElectionThreshold < 0 { + return errors.Errorf("VoterElectionThreshold must be greater than or equal to 0. Got %d", + params.VoterElectionThreshold) + } + if params.MaxTolerableByzantinePercentage <= 0 || params.MaxTolerableByzantinePercentage >= 34 { + return errors.Errorf("MaxTolerableByzantinePercentage must be in between 1 and 33. Got %d", + params.MaxTolerableByzantinePercentage) + } + if params.ElectionPrecision <= 1 || params.ElectionPrecision > 15 { + return errors.Errorf("ElectionPrecision must be in 2~15(including). Got %d", params.ElectionPrecision) + } + return nil +} + // DefaultBlockParams returns a default BlockParams. func DefaultBlockParams() BlockParams { return BlockParams{ diff --git a/types/params_test.go b/types/params_test.go index b446bda33..9fb11fa6a 100644 --- a/types/params_test.go +++ b/types/params_test.go @@ -132,3 +132,61 @@ func TestConsensusParamsUpdate(t *testing.T) { assert.Equal(t, tc.updatedParams, tc.params.Update(tc.updates)) } } + +func TestVoterParamsValidate(t *testing.T) { + errorCases := []VoterParams{ + { + VoterElectionThreshold: -1, + MaxTolerableByzantinePercentage: 1, + ElectionPrecision: 2, + }, + { + VoterElectionThreshold: 0, + MaxTolerableByzantinePercentage: 0, + ElectionPrecision: 2, + }, + { + VoterElectionThreshold: 0, + MaxTolerableByzantinePercentage: 34, + ElectionPrecision: 2, + }, + { + VoterElectionThreshold: 0, + MaxTolerableByzantinePercentage: 33, + ElectionPrecision: 1, + }, + { + VoterElectionThreshold: 0, + MaxTolerableByzantinePercentage: 33, + ElectionPrecision: 17, + }, + } + normalCases := []VoterParams{ + { + VoterElectionThreshold: 0, + MaxTolerableByzantinePercentage: 1, + ElectionPrecision: 2, + }, + { + VoterElectionThreshold: 99999999, + MaxTolerableByzantinePercentage: 1, + ElectionPrecision: 2, + }, + { + VoterElectionThreshold: 0, + MaxTolerableByzantinePercentage: 33, + ElectionPrecision: 2, + }, + { + VoterElectionThreshold: 0, + MaxTolerableByzantinePercentage: 1, + ElectionPrecision: 15, + }, + } + for _, tc := range errorCases { + assert.Error(t, tc.Validate()) + } + for _, tc := range normalCases { + assert.NoError(t, tc.Validate()) + } +} diff --git a/types/voter_set.go b/types/voter_set.go index 38781dcf5..dc2308e88 100644 --- a/types/voter_set.go +++ b/types/voter_set.go @@ -16,10 +16,6 @@ import ( tmrand "github.com/tendermint/tendermint/libs/rand" ) -var ( - MinVoters = 20 -) - // VoterSet represent a set of *Validator at a given height. type VoterSet struct { // NOTE: persisted via reflect, must be exported. @@ -408,10 +404,14 @@ func (c *candidate) SetWinPoint(winPoint int64) { c.val.VotingPower = winPoint } -func SelectVoter(validators *ValidatorSet, proofHash []byte) *VoterSet { - // TODO: decide MinVoters, MinTotalVotingPowerPercent; make it to config - if len(proofHash) == 0 || validators.Size() <= MinVoters { - // height 1 has voter set that is same to validator set +func accuracyFromElectionPrecision(precision int) float64 { + base := math.Pow10(precision) + result := (base - 1) / base + return result +} + +func SelectVoter(validators *ValidatorSet, proofHash []byte, voterParams *VoterParams) *VoterSet { + if len(proofHash) == 0 || validators.Size() <= voterParams.VoterElectionThreshold { return ToVoterAll(validators.Validators) } @@ -424,7 +424,13 @@ func SelectVoter(validators *ValidatorSet, proofHash []byte) *VoterSet { } } - winners := tmrand.RandomSamplingWithoutReplacement(seed, candidates, MinVoters) + minVoters := CalNumOfVoterToElect(int64(len(candidates)), float64(voterParams.MaxTolerableByzantinePercentage)/100, + accuracyFromElectionPrecision(voterParams.ElectionPrecision)) + if minVoters > math.MaxInt32 { + panic("CalNumOfVoterToElect is overflow for MaxInt32") + } + voterCount := tmmath.MaxInt(voterParams.VoterElectionThreshold, int(minVoters)) + winners := tmrand.RandomSamplingWithoutReplacement(seed, candidates, voterCount) voters := make([]*Validator, len(winners)) for i, winner := range winners { voters[i] = winner.(*candidate).val @@ -491,13 +497,14 @@ func RandVoterSet(numVoters int, votingPower int64) (*ValidatorSet, *VoterSet, [ } vals := NewValidatorSet(valz) sort.Sort(PrivValidatorsByAddress(privValidators)) - return vals, SelectVoter(vals, []byte{}), privValidators + return vals, SelectVoter(vals, []byte{}, DefaultVoterParams()), privValidators } // CalNumOfVoterToElect calculate the number of voter to elect and return the number. func CalNumOfVoterToElect(n int64, byzantineRatio float64, accuracy float64) int64 { if byzantineRatio < 0 || byzantineRatio > 1 || accuracy < 0 || accuracy > 1 { - panic("byzantineRatio and accuracy should be the float between 0 and 1") + panic(fmt.Sprintf("byzantineRatio and accuracy should be the float between 0 and 1. Got: %f", + byzantineRatio)) } byzantine := int64(math.Floor(float64(n) * byzantineRatio)) diff --git a/types/voter_set_test.go b/types/voter_set_test.go index 7e3a1b977..f6ee115ad 100644 --- a/types/voter_set_test.go +++ b/types/voter_set_test.go @@ -19,20 +19,68 @@ func countZeroStakingPower(vals []*Validator) int { return count } +func verifyVoterSetSame(t *testing.T, vset1, vset2 *VoterSet) { + assert.True(t, vset1.Size() == vset2.Size()) + for i, v1 := range vset1.Voters { + v2 := vset2.Voters[i] + assert.True(t, v1.Address.String() == v2.Address.String()) + assert.True(t, v1.VotingPower == v2.VotingPower) + assert.True(t, v1.StakingPower == v2.StakingPower) + } +} + +func verifyVoterSetDifferent(t *testing.T, vset1, vset2 *VoterSet) { + result := vset1.Size() != vset2.Size() + if !result { + for i, v1 := range vset1.Voters { + v2 := vset2.Voters[i] + if v1.Address.String() != v2.Address.String() || + v1.StakingPower != v2.StakingPower || + v1.VotingPower != v2.VotingPower { + result = true + break + } + } + } + assert.True(t, result) +} + func TestSelectVoter(t *testing.T) { - MinVoters = 29 valSet := randValidatorSet(30) + valSet.Validators[0].StakingPower = 0 + zeroVals := countZeroStakingPower(valSet.Validators) - for i := 0; i < 10000; i++ { - voterSet := SelectVoter(valSet, []byte{byte(i)}) - assert.True(t, voterSet.Size() >= 29-zeroVals) - if voterSet.totalVotingPower <= 0 { - for j := 0; j < voterSet.Size(); j++ { - // TODO solve this problem!!! - t.Logf("voter voting power = %d", voterSet.Voters[j].VotingPower) - } - } - assert.True(t, voterSet.TotalVotingPower() > 0) + genDoc := &GenesisDoc{ + GenesisTime: tmtime.Now(), + ChainID: "tendermint-test", + VoterParams: &VoterParams{10, 20, 1}, + Validators: toGenesisValidators(valSet.Validators), + } + hash := genDoc.Hash() + + // verifying determinism + voterSet1 := SelectVoter(valSet, hash, genDoc.VoterParams) + voterSet2 := SelectVoter(valSet, hash, genDoc.VoterParams) + verifyVoterSetSame(t, voterSet1, voterSet2) + + // verifying randomness + hash[0] = (hash[0] & 0xFE) | (^(hash[0] & 0x01) & 0x01) // reverse 1 bit of hash + voterSet3 := SelectVoter(valSet, hash, genDoc.VoterParams) + verifyVoterSetDifferent(t, voterSet1, voterSet3) + + // verifying zero-staking removed + assert.True(t, countZeroStakingPower(voterSet1.Voters) == 0) + + // case that all validators are voters + voterSet := SelectVoter(valSet, hash, &VoterParams{30, 1, 1}) + assert.True(t, voterSet.Size() == 30-zeroVals) + voterSet = SelectVoter(valSet, nil, genDoc.VoterParams) + assert.True(t, voterSet.Size() == 30-zeroVals) + + // test VoterElectionThreshold + for i := 1; i < 100; i++ { + voterSet := SelectVoter(valSet, hash, &VoterParams{15, i, 1}) + assert.True(t, voterSet.Size() >= 15) } } @@ -92,14 +140,14 @@ func findLargestStakingPowerGap(t *testing.T, loopCount int, minMaxRate int, max genDoc := &GenesisDoc{ GenesisTime: tmtime.Now(), ChainID: "tendermint-test", + VoterParams: DefaultVoterParams(), Validators: toGenesisValidators(valSet.Validators), } hash := genDoc.Hash() - MinVoters = maxVoters accumulation := make(map[string]int64) totalVoters := 0 for i := 0; i < loopCount; i++ { - voterSet := SelectVoter(valSet, hash) + voterSet := SelectVoter(valSet, hash, genDoc.VoterParams) for _, voter := range voterSet.Voters { accumulation[voter.Address.String()] += voter.StakingPower } @@ -132,10 +180,9 @@ func TestSelectVoterMaxVarious(t *testing.T) { t.Logf("<<< min: 100, max: %d >>>", 100*minMaxRate) for validators := 16; validators <= 256; validators *= 4 { for voters := 1; voters <= validators; voters += 10 { - MinVoters = voters valSet, _ := randValidatorSetWithMinMax(validators, 100, 100*int64(minMaxRate)) - voterSet := SelectVoter(valSet, []byte{byte(hash)}) - if voterSet.Size() < MinVoters { + voterSet := SelectVoter(valSet, []byte{byte(hash)}, &VoterParams{voters, 20, 5}) + if voterSet.Size() < voters { t.Logf("Cannot elect voters up to MaxVoters: validators=%d, MaxVoters=%d, actual voters=%d", validators, voters, voterSet.Size()) break @@ -160,3 +207,96 @@ func TestCalVotersNum(t *testing.T) { assert.Panics(t, func() { CalNumOfVoterToElect(total, 0.3, 10) }) assert.Panics(t, func() { CalNumOfVoterToElect(total, 1.1, 0.9999) }) } + +func makeByzantine(valSet *ValidatorSet, rate float64) map[string]bool { + result := make(map[string]bool) + byzantinePower := int64(0) + threshold := int64(float64(valSet.TotalStakingPower()) * rate) + for _, v := range valSet.Validators { + if byzantinePower+v.StakingPower > threshold { + break + } + result[v.Address.String()] = true + byzantinePower += v.StakingPower + } + return result +} + +func byzantinesPower(voters []*Validator, byzantines map[string]bool) int64 { + power := int64(0) + for _, v := range voters { + if byzantines[v.Address.String()] { + power += v.VotingPower + } + } + return power +} + +func countByzantines(voters []*Validator, byzantines map[string]bool) int { + count := 0 + for _, v := range voters { + if byzantines[v.Address.String()] { + count++ + } + } + return count +} + +func electVotersForLoop(t *testing.T, hash []byte, valSet *ValidatorSet, privMap map[string]PrivValidator, + byzantines map[string]bool, loopCount int, byzantinePercent, accuracy int) { + byzantineFault := 0 + totalVoters := 0 + totalByzantines := 0 + for i := 0; i < loopCount; i++ { + voterSet := SelectVoter(valSet, hash, &VoterParams{1, byzantinePercent, accuracy}) + byzantineThreshold := int64(float64(voterSet.TotalVotingPower())*0.33) + 1 + if byzantinesPower(voterSet.Voters, byzantines) >= byzantineThreshold { + byzantineFault++ + } + totalVoters += voterSet.Size() + totalByzantines += countByzantines(voterSet.Voters, byzantines) + proposer := valSet.SelectProposer(hash, int64(i), 0) + message := MakeRoundHash(hash, int64(i), 0) + proof, _ := privMap[proposer.Address.String()].GenerateVRFProof(message) + hash, _ = vrf.ProofToHash(proof) + } + t.Logf("[accuracy=%f] voters=%d, fault=%d, avg byzantines=%f", accuracyFromElectionPrecision(accuracy), + totalVoters/loopCount, byzantineFault, float64(totalByzantines)/float64(loopCount)) + assert.True(t, float64(byzantineFault) < float64(loopCount)*(1.0-accuracyFromElectionPrecision(accuracy))) +} + +func TestCalVotersNum2(t *testing.T) { + valSet, privMap := randValidatorSetWithMinMax(100, 100, 10000) + byzantinePercent := 20 + byzantines := makeByzantine(valSet, float64(byzantinePercent)/100) + genDoc := &GenesisDoc{ + GenesisTime: tmtime.Now(), + ChainID: "tendermint-test", + Validators: toGenesisValidators(valSet.Validators), + } + hash := genDoc.Hash() + + loopCount := 1000 + electVotersForLoop(t, hash, valSet, privMap, byzantines, loopCount, byzantinePercent, 1) + electVotersForLoop(t, hash, valSet, privMap, byzantines, loopCount, byzantinePercent, 2) + electVotersForLoop(t, hash, valSet, privMap, byzantines, loopCount, byzantinePercent, 3) + electVotersForLoop(t, hash, valSet, privMap, byzantines, loopCount, byzantinePercent, 4) + electVotersForLoop(t, hash, valSet, privMap, byzantines, loopCount, byzantinePercent, 5) +} + +func TestAccuracyFromElectionPrecision(t *testing.T) { + assert.True(t, accuracyFromElectionPrecision(2) == 0.99) + assert.True(t, accuracyFromElectionPrecision(3) == 0.999) + assert.True(t, accuracyFromElectionPrecision(4) == 0.9999) + assert.True(t, accuracyFromElectionPrecision(5) == 0.99999) + assert.True(t, accuracyFromElectionPrecision(6) == 0.999999) + assert.True(t, accuracyFromElectionPrecision(7) == 0.9999999) + assert.True(t, accuracyFromElectionPrecision(8) == 0.99999999) + assert.True(t, accuracyFromElectionPrecision(9) == 0.999999999) + assert.True(t, accuracyFromElectionPrecision(10) == 0.9999999999) + assert.True(t, accuracyFromElectionPrecision(11) == 0.99999999999) + assert.True(t, accuracyFromElectionPrecision(12) == 0.999999999999) + assert.True(t, accuracyFromElectionPrecision(13) == 0.9999999999999) + assert.True(t, accuracyFromElectionPrecision(14) == 0.99999999999999) + assert.True(t, accuracyFromElectionPrecision(15) == 0.999999999999999) +}