Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix the floating-point problem of sampling #94

Merged
merged 5 commits into from
Jun 29, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 24 additions & 22 deletions libs/rand/sampling.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package rand

import (
"fmt"
"math"
"math/big"
s "sort"
)
Expand Down Expand Up @@ -74,6 +73,12 @@ func moveWinnerToLast(candidates []Candidate, winner int) {

const uint64Mask = uint64(0x7FFFFFFFFFFFFFFF)

// precisionForSelection is a value to be corrected to increase precision when calculating voting power as an integer.
const precisionForSelection = uint64(1000)

// precisionCorrectionForSelection is a value corrected for accuracy of voting power
const precisionCorrectionForSelection = uint64(1000)

var divider *big.Int

func init() {
Expand All @@ -82,19 +87,13 @@ func init() {
}

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))
totalBig := new(big.Int).SetUint64(total)
a := new(big.Int).SetUint64(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) {

Expand Down Expand Up @@ -135,26 +134,29 @@ func RandomSamplingWithoutReplacement(
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])
correction := new(big.Int).SetUint64(totalPriority)
correction = correction.Mul(correction, new(big.Int).SetUint64(precisionForSelection))
compensationProportions := make([]big.Int, winnerNum)
for i := winnerNum - 2; i >= 0; i-- {
additionalCompensation := new(big.Int).Div(correction, new(big.Int).SetUint64(losersPriorities[i]))
compensationProportions[i].Add(&compensationProportions[i+1], additionalCompensation)
}
winners = candidates[len(candidates)-winnerNum:]
winPoints := make([]float64, len(winners))
totalWinPoint := float64(0)
recalibration := new(big.Int).Div(correction, new(big.Int).SetUint64(precisionCorrectionForSelection))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this same to recalibration := new(big.Int). SetUint64(totalPriority)? If so, we can remove the constant precisionCorrectionForSelection.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, but if the difference between each user's correction value is very small, it is indistinguishable. So, I reduced the correction value by precisionCorrectionForSelection(1000).

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))
// winPoint = correction + winner.Priority() * compensationProportions[i]
winPoint := new(big.Int).SetUint64(winner.Priority())
winPoint.Mul(winPoint, &compensationProportions[i])
winPoint.Add(winPoint, correction)

winner.SetWinPoint(winPoint.Div(winPoint, recalibration).Int64())
}
return winners
}

// sumTotalPriority calculate the sum of all candidate's priority(weight)
// and the sum should be less then or equal to MaxUint64
// TODO We need to check the total weight doesn't over MaxUint64 in somewhere not here.
func sumTotalPriority(candidates []Candidate) (sum uint64) {
for _, candi := range candidates {
sum += candi.Priority()
Expand Down
35 changes: 30 additions & 5 deletions libs/rand/sampling_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ func TestRandomSamplingWithoutReplacement1Candidate(t *testing.T) {
winners := RandomSamplingWithoutReplacement(0, candidates, 1)
assert.True(t, len(winners) == 1)
assert.True(t, candidates[0] == winners[0])
assert.True(t, winners[0].(*Element).winPoint == 1000)
assert.True(t, uint64(winners[0].(*Element).winPoint) == precisionForSelection)
resetWinPoint(candidates)

winners2 := RandomSamplingWithoutReplacement(0, candidates, 0)
Expand Down Expand Up @@ -176,11 +176,28 @@ func TestRandomSamplingWithoutReplacementIncludingZeroStakingPower(t *testing.T)
assert.True(t, len(winners2) == 90)
}

func accumulateAndResetReward(candidate []Candidate, acc []uint64) {
func TestRandomSamplingWithoutReplacementOverflow(t *testing.T) {
number := 100
candidates := newCandidates(number, func(i int) uint64 { return math.MaxUint64 / uint64(number) })
winners := RandomSamplingWithoutReplacement(rand.Uint64(), candidates, 64)
lastWinPoint := int64(math.MaxInt64)
for _, w := range winners {
element := w.(*Element)
assert.True(t, element.winPoint > 0)
assert.True(t, element.winPoint <= lastWinPoint)
lastWinPoint = element.winPoint
}
assert.Equal(t, lastWinPoint, int64(precisionForSelection))
}

func accumulateAndResetReward(candidate []Candidate, acc []uint64) uint64 {
totalWinPoint := uint64(0)
for i, c := range candidate {
acc[i] += uint64(c.(*Element).winPoint)
totalWinPoint += uint64(c.(*Element).winPoint)
c.(*Element).winPoint = 0
}
return totalWinPoint
}

func TestDivider(t *testing.T) {
Expand Down Expand Up @@ -277,14 +294,22 @@ func TestRandomSamplingWithoutReplacementEquity(t *testing.T) {

// good condition
candidates := newCandidates(100, func(i int) uint64 { return 1000000 + rand.Uint64()&0xFFFFF })
totalStaking := uint64(0)
for _, c := range candidates {
totalStaking += c.Priority()
}

accumulatedRewards := make([]uint64, 100)
totalAccumulateRewards := uint64(0)
for i := 0; i < loopCount; i++ {
RandomSamplingWithoutReplacement(uint64(i), candidates, 99)
accumulateAndResetReward(candidates, accumulatedRewards)
totalAccumulateRewards += accumulateAndResetReward(candidates, accumulatedRewards)
}
for i := 0; i < 99; i++ {
rewardPerStakingDiff :=
math.Abs(float64(accumulatedRewards[i])/float64(candidates[i].Priority())/float64(loopCount) - 1)
rewardRate := float64(accumulatedRewards[i]) / float64(totalAccumulateRewards)
stakingRate := float64(candidates[i].Priority()) / float64(totalStaking)
rate := rewardRate / stakingRate
rewardPerStakingDiff := math.Abs(1 - rate)
assert.True(t, rewardPerStakingDiff < 0.01)
}

Expand Down