-
Notifications
You must be signed in to change notification settings - Fork 28
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
feat: implement random sampling without replacement and staking power #83
Changes from all commits
fbc390c
9b8d758
d68124d
3749b65
521cdc1
5a4cf87
85a08e1
c99b7bd
41fe4c2
0f2d198
37a2431
52de880
b2247ba
8e74da4
8eda4c1
b85eedb
fcd158b
af8ea82
aac3c8b
9e8dc55
d5cf9b8
cbe2847
45e573e
cb7f045
a3835cb
b1f68f5
d930617
083f637
fdb7a02
e33896f
0a18969
f08edeb
08b7835
deb89eb
d30feec
06e84e8
4387cb6
94b2975
3efc1d7
1c759e3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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. | ||
// | ||
|
@@ -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] }) | ||
|
||
|
@@ -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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How can it be possible? And if possible(but I don't know why), I think to check who has zero priority value before calling this function is more better. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Some test case makes the situation such that a validator has zero staking. |
||
// 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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Putting the winners after the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Even if we create a new array for winners and take winners out of the candidates and put them in the winner list, the overrun references problem is the same unless we reduce the capacity of the candidates(we should re-allocate array to reduce the capacity). I don't know why I have to make a new array for winners. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Well, this is a trivial point, so I think it's also a good idea to reuse the tail in the area of the winner. I'm also slightly concerned that when it |
||
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 | ||
} | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is this change for?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ToVoterAll()
needs[]*Validator
notValidatorSet
.