Skip to content

Commit

Permalink
Merge pull request #83 from line/feature/voting_power
Browse files Browse the repository at this point in the history
feat: implement random sampling without replacement and staking power
  • Loading branch information
Woosang Son authored Jun 16, 2020
2 parents 50fd926 + 1c759e3 commit 1121e8e
Show file tree
Hide file tree
Showing 39 changed files with 824 additions and 448 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG_PENDING.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,12 @@

- Go API

- [types] [\#83](https://github.com/line/tendermint/pull/83) Add `StakingPower` to `Validator`
- [consensus] [\#83](https://github.com/line/tendermint/pull/83) Change calculation of `VotingPower`

### 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

### IMPROVEMENTS:

Expand Down
2 changes: 1 addition & 1 deletion blockchain/v0/reactor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ func randGenesisDoc(numValidators int, randPower bool, minPower int64) (*types.G
val, privVal := types.RandValidator(randPower, minPower)
validators[i] = types.GenesisValidator{
PubKey: val.PubKey,
Power: val.VotingPower,
Power: val.StakingPower,
}
privValidators[i] = privVal
}
Expand Down
2 changes: 1 addition & 1 deletion blockchain/v1/reactor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ func randGenesisDoc(numValidators int, randPower bool, minPower int64) (*types.G
val, privVal := types.RandValidator(randPower, minPower)
validators[i] = types.GenesisValidator{
PubKey: val.PubKey,
Power: val.VotingPower,
Power: val.StakingPower,
}
privValidators[i] = privVal
}
Expand Down
2 changes: 1 addition & 1 deletion blockchain/v2/reactor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -464,7 +464,7 @@ func randGenesisDoc(chainID string, numValidators int, randPower bool, minPower
val, privVal := types.RandValidator(randPower, minPower)
validators[i] = types.GenesisValidator{
PubKey: val.PubKey,
Power: val.VotingPower,
Power: val.StakingPower,
}
privValidators[i] = privVal
}
Expand Down
2 changes: 1 addition & 1 deletion consensus/common_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -804,7 +804,7 @@ func randGenesisDoc(numValidators int, randPower bool, minPower int64) (*types.G
val, privVal := types.RandValidator(randPower, minPower)
validators[i] = types.GenesisValidator{
PubKey: val.PubKey,
Power: val.VotingPower,
Power: val.StakingPower,
}
privValidators[i] = privVal
}
Expand Down
6 changes: 3 additions & 3 deletions consensus/reactor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -335,7 +335,7 @@ func TestReactorRecordsVotesAndBlockParts(t *testing.T) {
//-------------------------------------------------------------
// ensure we can make blocks despite cycling a validator set

func TestReactorVotingPowerChange(t *testing.T) {
func TestReactorStakingPowerChange(t *testing.T) {
nVals := 4
logger := log.TestingLogger()
css, cleanup := randConsensusNet(
Expand Down Expand Up @@ -377,7 +377,7 @@ func TestReactorVotingPowerChange(t *testing.T) {

if css[0].GetRoundState().LastVoters.TotalVotingPower() == previousTotalVotingPower {
t.Fatalf(
"expected voting power to change (before: %d, after: %d)",
"expected staking power to change (before: %d, after: %d)",
previousTotalVotingPower,
css[0].GetRoundState().LastVoters.TotalVotingPower())
}
Expand Down Expand Up @@ -486,7 +486,7 @@ func TestReactorValidatorSetChanges(t *testing.T) {

if css[nVals].GetRoundState().LastVoters.TotalVotingPower() == previousTotalVotingPower {
t.Errorf(
"expected voting power to change (before: %d, after: %d)",
"expected staking power to change (before: %d, after: %d)",
previousTotalVotingPower,
css[nVals].GetRoundState().LastVoters.TotalVotingPower())
}
Expand Down
2 changes: 1 addition & 1 deletion consensus/replay.go
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,7 @@ func (h *Handshaker) ReplayBlocks(
return nil, err
}
state.Validators = types.NewValidatorSet(vals)
state.Voters = types.ToVoterAll(state.Validators)
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())
Expand Down
3 changes: 1 addition & 2 deletions consensus/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ func StateMetrics(metrics *Metrics) StateOption {
// String returns a string.
func (cs *State) String() string {
// better not to access shared variables
return "ConsensusState" //(H:%v R:%v S:%v", cs.Height, cs.Round, cs.Step)
return "ConsensusState" // (H:%v R:%v S:%v", cs.Height, cs.Round, cs.Step)
}

// GetState returns a copy of the chain state.
Expand Down Expand Up @@ -1557,7 +1557,6 @@ func (cs *State) recordMetrics(height int64, block *types.Block) {
cs.Logger.Error("Error on retrival of pubkey", "err", err)
continue
}

if bytes.Equal(val.Address, pubKey.Address()) {
label := []string{
"validator_address", val.Address.String(),
Expand Down
2 changes: 1 addition & 1 deletion consensus/state_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ func TestStateBadProposal(t *testing.T) {
proposalCh := subscribe(cs1.eventBus, types.EventQueryCompleteProposal)
voteCh := subscribe(cs1.eventBus, types.EventQueryVote)

propBlock, _ := cs1.createProposalBlock(round) //changeProposer(t, cs1, vs2)
propBlock, _ := cs1.createProposalBlock(round) // changeProposer(t, cs1, vs2)

// make the second validator the proposer by incrementing round
round++
Expand Down
2 changes: 1 addition & 1 deletion evidence/pool.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ func (evpool *Pool) AddEvidence(evidence types.Evidence) error {
return err
}
_, val := valSet.GetByAddress(evidence.Address())
priority := val.VotingPower
priority := val.StakingPower

_, err = evpool.store.AddNewEvidence(evidence, priority)
if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion evidence/pool_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ func initializeValidatorState(valAddr []byte, height int64) dbm.DB {

// create validator set and state
vals := []*types.Validator{
{Address: valAddr, VotingPower: 1},
{Address: valAddr, StakingPower: 1},
}
state := sm.State{
LastBlockHeight: 0,
Expand Down
104 changes: 77 additions & 27 deletions libs/rand/sampling.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,18 @@ package rand

import (
"fmt"
"math"
"math/big"
s "sort"
)

// Interface for performing weighted deterministic random selection.
type Candidate interface {
Priority() uint64
LessThan(other Candidate) bool
IncreaseWin()
SetWinPoint(winPoint int64)
}

const uint64Mask = uint64(0x7FFFFFFFFFFFFFFF)

// Select a specified number of candidates randomly from the candidate set based on each priority. This function is
// deterministic and will produce the same result for the same input.
//
Expand All @@ -33,7 +33,7 @@ func RandomSamplingWithPriority(
thresholds := make([]uint64, sampleSize)
for i := 0; i < sampleSize; i++ {
// calculating [gross weights] × [(0,1] random number]
thresholds[i] = uint64(float64(nextRandom(&seed)&uint64Mask) / float64(uint64Mask+1) * float64(totalPriority))
thresholds[i] = randomThreshold(&seed, totalPriority)
}
s.Slice(thresholds, func(i, j int) bool { return thresholds[i] < thresholds[j] })

Expand Down Expand Up @@ -66,52 +66,102 @@ func RandomSamplingWithPriority(
totalPriority, actualTotalPriority, seed, sampleSize, undrawn, undrawn, thresholds[undrawn], len(candidates)))
}

const MaxSamplingLoopTry = 1000
func moveWinnerToLast(candidates []Candidate, winner int) {
winnerCandidate := candidates[winner]
copy(candidates[winner:], candidates[winner+1:])
candidates[len(candidates)-1] = winnerCandidate
}

const uint64Mask = uint64(0x7FFFFFFFFFFFFFFF)

// `RandomSamplingToMax` elects voters among candidates so it updates wins of candidates
// Voters can be elected by a maximum `limitCandidates`.
// However, if the likely candidates are less than the `limitCandidates`,
// the number of voters may be less than the `limitCandidates`.
// This is to prevent falling into an infinite loop.
func RandomSamplingToMax(
seed uint64, candidates []Candidate, limitCandidates int, totalPriority uint64) uint64 {
var divider *big.Int

if len(candidates) < limitCandidates {
panic("The number of candidates cannot be less limitCandidate")
func init() {
divider = big.NewInt(int64(uint64Mask))
divider.Add(divider, big.NewInt(1))
}

func randomThreshold(seed *uint64, total uint64) uint64 {
if int64(total) < 0 {
panic(fmt.Sprintf("total priority is overflow: %d", total))
}
totalBig := big.NewInt(int64(total))
a := big.NewInt(int64(nextRandom(seed) & uint64Mask))
a.Mul(a, totalBig)
a.Div(a, divider)
return a.Uint64()
}

// `RandomSamplingWithoutReplacement` elects winners among candidates without replacement
// so it updates rewards of winners. This function continues to elect winners until the both of two
// conditions(minSamplingCount, minPriorityPercent) are met.
func RandomSamplingWithoutReplacement(
seed uint64, candidates []Candidate, minSamplingCount int) (winners []Candidate) {

if len(candidates) < minSamplingCount {
panic(fmt.Sprintf("The number of candidates(%d) cannot be less minSamplingCount %d",
len(candidates), minSamplingCount))
}

totalPriority := sumTotalPriority(candidates)
candidates = sort(candidates)
totalSampling := uint64(0)
winCandidates := make(map[Candidate]bool)
for len(winCandidates) < limitCandidates && totalSampling < MaxSamplingLoopTry {
threshold := uint64(float64(nextRandom(&seed)&uint64Mask) / float64(uint64Mask+1) * float64(totalPriority))
winnersPriority := uint64(0)
losersPriorities := make([]uint64, len(candidates))
winnerNum := 0
for winnerNum < minSamplingCount {
if totalPriority-winnersPriority == 0 {
// it's possible if some candidates have zero priority
// if then, we can't elect voter any more; we should holt electing not to fall in infinity loop
break
}
threshold := randomThreshold(&seed, totalPriority-winnersPriority)
cumulativePriority := uint64(0)
found := false
for _, candidate := range candidates {
for i, candidate := range candidates[:len(candidates)-winnerNum] {
if threshold < cumulativePriority+candidate.Priority() {
if !winCandidates[candidate] {
winCandidates[candidate] = true
}
candidate.IncreaseWin()
totalSampling++
moveWinnerToLast(candidates, i)
winnersPriority += candidate.Priority()
losersPriorities[winnerNum] = totalPriority - winnersPriority
winnerNum++
found = true
break
}
cumulativePriority += candidate.Priority()
}

if !found {
panic(fmt.Sprintf("Cannot find random sample. totalPriority may be wrong: totalPriority=%d, "+
"actualTotalPriority=%d, threshold=%d", totalPriority, sumTotalPriority(candidates), threshold))
panic(fmt.Sprintf("Cannot find random sample. winnerNum=%d, minSamplingCount=%d, "+
"winnersPriority=%d, totalPriority=%d, threshold=%d",
winnerNum, minSamplingCount, winnersPriority, totalPriority, threshold))
}
}
compensationProportions := make([]float64, winnerNum)
for i := winnerNum - 2; i >= 0; i-- { // last winner doesn't get compensation reward
compensationProportions[i] = compensationProportions[i+1] + 1/float64(losersPriorities[i])
}
winners = candidates[len(candidates)-winnerNum:]
winPoints := make([]float64, len(winners))
totalWinPoint := float64(0)
for i, winner := range winners {
winPoints[i] = 1 + float64(winner.Priority())*compensationProportions[i]
totalWinPoint += winPoints[i]
}
for i, winner := range winners {
if winPoints[i] > math.MaxInt64 || winPoints[i] < 0 {
panic(fmt.Sprintf("winPoint is invalid: %f", winPoints[i]))
}
winner.SetWinPoint(int64(float64(totalPriority) * winPoints[i] / totalWinPoint))
}
return totalSampling
return winners
}

func sumTotalPriority(candidates []Candidate) (sum uint64) {
for _, candi := range candidates {
sum += candi.Priority()
}
if sum == 0 {
panic("all candidates have zero priority")
}
return
}

Expand Down
Loading

0 comments on commit 1121e8e

Please sign in to comment.