diff --git a/libs/rand/sampling.go b/libs/rand/sampling.go index 2f4000259..4d7f85a0b 100644 --- a/libs/rand/sampling.go +++ b/libs/rand/sampling.go @@ -2,7 +2,6 @@ package rand import ( "fmt" - "math" "math/big" s "sort" ) @@ -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() { @@ -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) { @@ -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)) 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() diff --git a/libs/rand/sampling_test.go b/libs/rand/sampling_test.go index 1783b5801..e7b261907 100644 --- a/libs/rand/sampling_test.go +++ b/libs/rand/sampling_test.go @@ -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) @@ -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) { @@ -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) }