Skip to content

Commit b3fcb43

Browse files
Merge pull request #178 from keep-network/rng-test
Overhaul the RNG tests
2 parents dddd689 + d75f343 commit b3fcb43

1 file changed

Lines changed: 83 additions & 7 deletions

File tree

test/rngTest.js

Lines changed: 83 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)