@@ -11,6 +11,7 @@ describe("RNG", () => {
1111
1212 describe ( "bitsRequired" , async ( ) => {
1313 it ( "Returns the number of bits required" , async ( ) => {
14+ // Any number higher than 2**32 - 1 will return 32
1415 expect ( await rngInstance . bitsRequired ( 2 ** 32 + 1 ) ) . to . be . equal ( 32 )
1516 expect ( await rngInstance . bitsRequired ( 2 ** 32 ) ) . to . be . equal ( 32 )
1617 expect ( await rngInstance . bitsRequired ( 2 ** 32 - 1 ) ) . to . be . equal ( 32 )
@@ -28,26 +29,59 @@ describe("RNG", () => {
2829 expect ( await rngInstance . bitsRequired ( 2 ** 2 - 1 ) ) . to . be . equal ( 2 )
2930
3031 expect ( await rngInstance . bitsRequired ( 2 ) ) . to . be . equal ( 1 )
32+
33+ // Generative Testing
34+ const numberOfSamples = 100
35+ const maxNumber = 2 ** 32 - 1
36+ for ( let i = 0 ; i < numberOfSamples ; i ++ ) {
37+ const randomNumber = Math . floor ( Math . random ( ) * maxNumber )
38+ const bitsRequired = await rngInstance . bitsRequired ( randomNumber )
39+ // We can represent 0 and 1 with 1 bit, 0-3 with 2 bits, 0-7 with 3
40+ // bits, etc. Every bit doubles the number of numbers we can represent,
41+ // so working backwards is log base 2.
42+ const expectation = Math . ceil ( Math . log2 ( randomNumber ) )
43+ expect ( bitsRequired ) . to . equal (
44+ expectation ,
45+ `something went wrong calculating the bits required for ${ randomNumber } ` ,
46+ )
47+ }
3148 } )
3249 } )
3350
3451 describe ( "truncate" , async ( ) => {
3552 it ( "Truncates a number to the correct number of bits" , async ( ) => {
36- a = 0xffffffff
53+ const a = 0xffffffff
3754
38- b = await rngInstance . truncate ( 1 , a )
39- c = await rngInstance . truncate ( 2 , a )
40- d = await rngInstance . truncate ( 16 , a )
41- e = await rngInstance . truncate ( 31 , a )
42- f = await rngInstance . truncate ( 32 , a )
43- g = await rngInstance . truncate ( 64 , a )
55+ const b = await rngInstance . truncate ( 1 , a )
56+ const c = await rngInstance . truncate ( 2 , a )
57+ const d = await rngInstance . truncate ( 16 , a )
58+ const e = await rngInstance . truncate ( 31 , a )
59+ const f = await rngInstance . truncate ( 32 , a )
60+ const g = await rngInstance . truncate ( 64 , a )
4461
4562 expect ( b ) . to . be . equal ( 0x1 )
4663 expect ( c ) . to . be . equal ( 0x3 )
4764 expect ( d ) . to . be . equal ( 0xffff )
4865 expect ( e ) . to . be . equal ( 0x7fffffff )
4966 expect ( f ) . to . be . equal ( a )
5067 expect ( g ) . to . be . equal ( a )
68+
69+ // Generative Testing
70+ const numberOfSamples = 3
71+ const maxNumber = 2 ** 32 - 1
72+ for ( let i = 0 ; i < numberOfSamples ; i ++ ) {
73+ // Truncating to `bits` is mathematically equivalent to
74+ // `number % (2 ** bits)`
75+ for ( let bits = 1 ; bits < 65 ; bits ++ ) {
76+ const randomNumber = Math . floor ( Math . random ( ) * maxNumber )
77+ const truncatedNumber = await rngInstance . truncate ( bits , randomNumber )
78+ const expectation = randomNumber % 2 ** bits
79+ expect ( truncatedNumber ) . to . be . equal (
80+ expectation ,
81+ `something went wrong truncating ${ randomNumber } to ${ bits } bits` ,
82+ )
83+ }
84+ }
5185 } )
5286 } )
5387
@@ -62,5 +96,47 @@ describe("RNG", () => {
6296 const num = ethers . BigNumber . from ( hex )
6397 expect ( num ) . to . be . below ( r )
6498 } )
99+
100+ it ( "Produces a uniform distribution" , async ( ) => {
101+ // Algorithm adapted from
102+ // https://www.rosettacode.org/wiki/Verify_distribution_uniformity/Naive#JavaScript
103+ //
104+ // We sample the distribution, and then make sure that the count of any
105+ // generated number doesn't exceed a tolerance.
106+
107+ const delta = 20 // percentage degree of inaccuracy to tolerate
108+ const maxSeed = 2 ** 32
109+
110+ // Use a low number of possible outcomes but a large number of samples to
111+ // make good use of the time-costly runtime to evaluate uniformity.
112+ // Higher number of possible outcomes would require higher number of
113+ // samples.
114+ const maxNumber = 6
115+ const numSamples = 1000
116+ const count = { }
117+ for ( let i = 0 ; i < numSamples ; i ++ ) {
118+ const val = await rngInstance . getIndex (
119+ maxNumber ,
120+ Math . floor ( Math . random ( ) * maxSeed ) ,
121+ )
122+ // Count the number of occurences of each random number
123+ count [ val ] = ( count [ val ] || 0 ) + 1
124+ }
125+ const vals = Object . keys ( count )
126+
127+ const target = numSamples / maxNumber
128+
129+ // Tolerate counts that are within delta% of true uniformity
130+ const tolerance = ( target * delta ) / 100
131+
132+ for ( let i = 0 ; i < vals . length ; i ++ ) {
133+ const val = vals [ i ]
134+ const distance = Math . abs ( count [ val ] - target )
135+ expect ( distance ) . to . be . below (
136+ tolerance ,
137+ `${ val } 's count of ${ count [ val ] } ] was too far away from the uniform expectation of ${ target } ` ,
138+ )
139+ }
140+ } )
65141 } )
66142} )
0 commit comments