/* * Copyright (c) 2014, Yawning Angel * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * * Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ // Package probdist implements a weighted probability distribution suitable for // protocol parameterization. To allow for easy reproduction of a given // distribution, the drbg package is used as the random number source. package probdist // import "gitlab.com/yawning/obfs4.git/common/probdist" import ( "bytes" "container/list" "fmt" "math/rand" "sync" "gitlab.com/yawning/obfs4.git/common/csrand" "gitlab.com/yawning/obfs4.git/common/drbg" ) const ( minValues = 1 maxValues = 100 ) // WeightedDist is a weighted distribution. type WeightedDist struct { sync.Mutex minValue int maxValue int biased bool values []int weights []float64 alias []int prob []float64 } // New creates a weighted distribution of values ranging from min to max // based on a HashDrbg initialized with seed. Optionally, bias the weight // generation to match the ScrambleSuit non-uniform distribution from // obfsproxy. func New(seed *drbg.Seed, min, max int, biased bool) (w *WeightedDist) { w = &WeightedDist{minValue: min, maxValue: max, biased: biased} if max <= min { panic(fmt.Sprintf("wDist.Reset(): min >= max (%d, %d)", min, max)) } w.Reset(seed) return } // genValues creates a slice containing a random number of random values // that when scaled by adding minValue will fall into [min, max]. func (w *WeightedDist) genValues(rng *rand.Rand) { nValues := (w.maxValue + 1) - w.minValue values := rng.Perm(nValues) if nValues < minValues { nValues = minValues } if nValues > maxValues { nValues = maxValues } nValues = rng.Intn(nValues) + 1 w.values = values[:nValues] } // genBiasedWeights generates a non-uniform weight list, similar to the // ScrambleSuit prob_dist module. func (w *WeightedDist) genBiasedWeights(rng *rand.Rand) { w.weights = make([]float64, len(w.values)) culmProb := 0.0 for i := range w.weights { p := (1.0 - culmProb) * rng.Float64() w.weights[i] = p culmProb += p } } // genUniformWeights generates a uniform weight list. func (w *WeightedDist) genUniformWeights(rng *rand.Rand) { w.weights = make([]float64, len(w.values)) for i := range w.weights { w.weights[i] = rng.Float64() } } // genTables calculates the alias and prob tables used for Vose's Alias method. // Algorithm taken from http://www.keithschwarz.com/darts-dice-coins/ func (w *WeightedDist) genTables() { n := len(w.weights) var sum float64 for _, weight := range w.weights { sum += weight } // Create arrays $Alias$ and $Prob$, each of size $n$. alias := make([]int, n) prob := make([]float64, n) // Create two worklists, $Small$ and $Large$. small := list.New() large := list.New() scaled := make([]float64, n) for i, weight := range w.weights { // Multiply each probability by $n$. p_i := weight * float64(n) / sum scaled[i] = p_i // For each scaled probability $p_i$: if scaled[i] < 1.0 { // If $p_i < 1$, add $i$ to $Small$. small.PushBack(i) } else { // Otherwise ($p_i \ge 1$), add $i$ to $Large$. large.PushBack(i) } } // While $Small$ and $Large$ are not empty: ($Large$ might be emptied first) for small.Len() > 0 && large.Len() > 0 { // Remove the first element from $Small$; call it $l$. l := small.Remove(small.Front()).(int) // Remove the first element from $Large$; call it $g$. g := large.Remove(large.Front()).(int) // Set $Prob[l] = p_l$. prob[l] = scaled[l] // Set $Alias[l] = g$. alias[l] = g // Set $p_g := (p_g + p_l) - 1$. (This is a more numerically stable option.) scaled[g] = (scaled[g] + scaled[l]) - 1.0 if scaled[g] < 1.0 { // If $p_g < 1$, add $g$ to $Small$. small.PushBack(g) } else { // Otherwise ($p_g \ge 1$), add $g$ to $Large$. large.PushBack(g) } } // While $Large$ is not empty: for large.Len() > 0 { // Remove the first element from $Large$; call it $g$. g := large.Remove(large.Front()).(int) // Set $Prob[g] = 1$. prob[g] = 1.0 } // While $Small$ is not empty: This is only possible due to numerical instability. for small.Len() > 0 { // Remove the first element from $Small$; call it $l$. l := small.Remove(small.Front()).(int) // Set $Prob[l] = 1$. prob[l] = 1.0 } w.prob = prob w.alias = alias } // Reset generates a new distribution with the same min/max based on a new // seed. func (w *WeightedDist) Reset(seed *drbg.Seed) { // Initialize the deterministic random number generator. drbg, _ := drbg.NewHashDrbg(seed) rng := rand.New(drbg) w.Lock() defer w.Unlock() w.genValues(rng) if w.biased { w.genBiasedWeights(rng) } else { w.genUniformWeights(rng) } w.genTables() } // Sample generates a random value according to the distribution. func (w *WeightedDist) Sample() int { var idx int w.Lock() defer w.Unlock() // Generate a fair die roll from an $n$-sided die; call the side $i$. i := csrand.Intn(len(w.values)) // Flip a biased coin that comes up heads with probability $Prob[i]$. if csrand.Float64() <= w.prob[i] { // If the coin comes up "heads," return $i$. idx = i } else { // Otherwise, return $Alias[i]$. idx = w.alias[i] } return w.minValue + w.values[idx] } // String returns a dump of the distribution table. func (w *WeightedDist) String() string { var buf bytes.Buffer buf.WriteString("[ ") for i, v := range w.values { p := w.weights[i] if p > 0.01 { // Squelch tiny probabilities. buf.WriteString(fmt.Sprintf("%d: %f ", v, p)) } } buf.WriteString("]") return buf.String() }