pool_of_entropy 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/.travis.yml +7 -0
- data/DIEHARDER_TEST.md +156 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +151 -0
- data/Rakefile +10 -0
- data/lib/pool_of_entropy.rb +160 -0
- data/lib/pool_of_entropy/core_prng.rb +161 -0
- data/lib/pool_of_entropy/version.rb +3 -0
- data/pool_of_entropy.gemspec +26 -0
- data/spec/core_prng_spec.rb +541 -0
- data/spec/pool_of_entropy_spec.rb +362 -0
- data/spec/spec_helper.rb +7 -0
- metadata +134 -0
@@ -0,0 +1,161 @@
|
|
1
|
+
require 'digest/sha2'
|
2
|
+
require 'securerandom'
|
3
|
+
|
4
|
+
# This class implements a random number generator based on SHA-512
|
5
|
+
#
|
6
|
+
# An object of the class has internal state that is modified on each call to
|
7
|
+
# #update or any #read_... method (internally the #read_... methods call #update).
|
8
|
+
# The #read_... methods generate a pseudo-random number from the current
|
9
|
+
# state pool using SHA-512 and use it in the return value. It is not feasible to
|
10
|
+
# determine the internal state from the pseudo-random data received, and not
|
11
|
+
# possible to manipulate results from the PRNG in a predictable manner without
|
12
|
+
# knowing the internal state.
|
13
|
+
#
|
14
|
+
# @example Using default internal state, initialised using SecureRandom.random_bytes
|
15
|
+
# prng = PoolOfEntropy::CorePRNG.new
|
16
|
+
# prng.read_bytes
|
17
|
+
# # E.g. => "]\x12\x9E\xF5\x17\xF3\xC2\x1A\x15\xDFu]\x95\nd\x12"
|
18
|
+
# prng.read_hex
|
19
|
+
# # E.g. => "a0e00d2848242ec49e0a15ef411ba647"
|
20
|
+
# prng.read_bignum
|
21
|
+
# # E.g. => 33857278877368906880463811096418580004
|
22
|
+
# prng.read_float
|
23
|
+
# # E.g. => 0.6619838265836278
|
24
|
+
# prng.generate_integer( 20 )
|
25
|
+
# # E.g. => 7
|
26
|
+
#
|
27
|
+
|
28
|
+
class PoolOfEntropy::CorePRNG
|
29
|
+
|
30
|
+
# Creates a new random number source. All parameters are optional.
|
31
|
+
# @param [Integer] size Number of 64-byte blocks, between 1 and 256 (16KB)
|
32
|
+
# @param [String] initial_state Sets contents of state pool (default uses SecureRandom)
|
33
|
+
# @param [Integer] mix_block_id
|
34
|
+
# @return [PoolOfEntropy::CorePRNG]
|
35
|
+
def initialize size = 1, initial_state = SecureRandom.random_bytes( 64 * Integer(size) ), mix_block_id = 0
|
36
|
+
@size = Integer( size )
|
37
|
+
if @size < 1 || @size > 256
|
38
|
+
raise ArgumentError, "Size of pool must be in Range 1..256, got #{@size}"
|
39
|
+
end
|
40
|
+
unless initial_state.is_a? String
|
41
|
+
raise TypeError, "Initial state must be a String, got #{initial_state.inspect}"
|
42
|
+
end
|
43
|
+
@state = initial_state.clone
|
44
|
+
@state.force_encoding( 'BINARY' )
|
45
|
+
|
46
|
+
if @state.size != size * 64
|
47
|
+
raise ArgumentError, "Initial state bad size - expected #{size * 64} bytes, got #{@state.size} bytes"
|
48
|
+
end
|
49
|
+
|
50
|
+
@mix_block_id = Integer( mix_block_id ) % @size
|
51
|
+
end
|
52
|
+
|
53
|
+
# The number of 64-byte blocks used in the internal state pool.
|
54
|
+
# @return [Integer]
|
55
|
+
attr_reader :size
|
56
|
+
|
57
|
+
# Identifies the next 64-byte block in the pool that will be altered
|
58
|
+
# by a read or update process.
|
59
|
+
# @return [Integer]
|
60
|
+
attr_reader :mix_block_id
|
61
|
+
|
62
|
+
# A clone of the internal state pool. In combination with #mix_block_id, describes
|
63
|
+
# the whole PRNG. If this value is supplied to an end user, then they can easily
|
64
|
+
# predict future values of the PRNG.
|
65
|
+
# @return [PoolOfEntropy::CorePRNG]
|
66
|
+
def state
|
67
|
+
@state.clone
|
68
|
+
end
|
69
|
+
|
70
|
+
# The clone of a PoolOfEntropy::CorePRNG object includes separate copy of internal state
|
71
|
+
# @return [PoolOfEntropy::CorePRNG]
|
72
|
+
def clone
|
73
|
+
PoolOfEntropy::CorePRNG.new( self.size, self.state, self.mix_block_id )
|
74
|
+
end
|
75
|
+
|
76
|
+
# Mixes supplied data into the curent state. This is called
|
77
|
+
# internally by #read_... methods as well.
|
78
|
+
# @param [String] data Data to be mixed. Note empty string '' and nil are equivalent and *do* change the state.
|
79
|
+
# @return [nil]
|
80
|
+
def update data
|
81
|
+
new_block = Digest::SHA512.digest( @state + data.to_s )
|
82
|
+
@state[64*@mix_block_id,64] = new_block
|
83
|
+
@mix_block_id = (@mix_block_id + 1) % @size
|
84
|
+
nil
|
85
|
+
end
|
86
|
+
|
87
|
+
# Statistically flat distribution of 128 bits (16 bytes)
|
88
|
+
# @param [Array<String>] adjustments mixed in using SHA-512, so that they affect return value, but not internal state
|
89
|
+
# @return [String] 16 characters in ASCII-8BIT encoding
|
90
|
+
def read_bytes *adjustments
|
91
|
+
raw_digest = Digest::SHA512.digest( @state )
|
92
|
+
self.update( raw_digest )
|
93
|
+
adjustments.compact.each do |adjust|
|
94
|
+
raw_digest = Digest::SHA512.digest( raw_digest + adjust )
|
95
|
+
end
|
96
|
+
fold_bits( fold_bits( raw_digest ) )
|
97
|
+
end
|
98
|
+
|
99
|
+
# Statistically flat distribution of 32 hex digits
|
100
|
+
# @param [Array<String>] adjustments mixed in using SHA-512, so that they affect return value, but not internal state
|
101
|
+
# @return [String] 32 hex digits
|
102
|
+
def read_hex *adjustments
|
103
|
+
read_bytes( *adjustments ).unpack('H*').first
|
104
|
+
end
|
105
|
+
|
106
|
+
# Statistically flat distribution from range 0...2**128
|
107
|
+
# @param [Array<String>] adjustments mixed in using SHA-512, so that they affect return value, but not internal state
|
108
|
+
# @return [Bignum,Fixnum] between 0 and 0xffffffffffffffffffffffffffffffff
|
109
|
+
def read_bignum *adjustments
|
110
|
+
nums = read_bytes( *adjustments ).unpack('Q>*')
|
111
|
+
nums.inject(0) { |sum,v| (sum << 64) + v }
|
112
|
+
end
|
113
|
+
|
114
|
+
# Statistically flat distribution from interval 0.0...1.0, with 53-bit precision
|
115
|
+
# @param [Array<String>] adjustments mixed in using SHA-512, so that they affect return value, but not internal state
|
116
|
+
# @return [Float] between 0.0 and 0.9999999999999999
|
117
|
+
def read_float *adjustments
|
118
|
+
num = read_bytes( *adjustments ).unpack('Q>*').first >> 11
|
119
|
+
num.to_f / 2 ** 53
|
120
|
+
end
|
121
|
+
|
122
|
+
# Statistically flat distribution from range (0...top).
|
123
|
+
# If necessary, it will read more data to ensure absolute fairness. This method
|
124
|
+
# can generate an unbiased distribution of Bignums up to roughly half the maximum bit size
|
125
|
+
# allowed by Ruby (i.e. much larger than 2**128 generated in a single read)
|
126
|
+
# @param [Fixnum,Bignum] top upper bound of distribution, not inclusive
|
127
|
+
# @param [Array<String>] adjustments mixed in using SHA-512, so that they affect return value, but not internal state
|
128
|
+
# @return [Fixnum,Bignum] between 0 and top-1 inclusive
|
129
|
+
def generate_integer top, *adjustments
|
130
|
+
power = 1
|
131
|
+
sum = 0
|
132
|
+
lower_bound = 0
|
133
|
+
words = []
|
134
|
+
|
135
|
+
loop do
|
136
|
+
words = read_bytes( *adjustments ).unpack('L>*') if words.empty?
|
137
|
+
sum = 2**32 * sum + words.shift
|
138
|
+
power *= 2**32
|
139
|
+
lower_bound = sum * top / power
|
140
|
+
upper_bound = ( (sum + 1) * top ) / power
|
141
|
+
break if lower_bound == upper_bound
|
142
|
+
end
|
143
|
+
|
144
|
+
lower_bound
|
145
|
+
end
|
146
|
+
|
147
|
+
private
|
148
|
+
|
149
|
+
# Xors first half of a message with second half. Only works for messages
|
150
|
+
# which are multiples of 8 bytes long.
|
151
|
+
# @param [String] msg bytes to fold
|
152
|
+
# @return [String] folded message, half the length of original
|
153
|
+
def fold_bits msg
|
154
|
+
l = msg.length/2
|
155
|
+
folded_32bits = msg[0,l].unpack('L>*').zip( msg[l,l].unpack('L>*') ).map do |x,y|
|
156
|
+
x ^ y
|
157
|
+
end
|
158
|
+
folded_32bits.pack('L>*')
|
159
|
+
end
|
160
|
+
|
161
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'pool_of_entropy/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "pool_of_entropy"
|
8
|
+
spec.version = PoolOfEntropy::VERSION
|
9
|
+
spec.authors = ["Neil Slater"]
|
10
|
+
spec.email = ["slobo777@gmail.com"]
|
11
|
+
spec.summary = %q{Random number generator with extra features for gamers.}
|
12
|
+
spec.description = %q{PoolOfEntropy is a PRNG based on cryptographic secure PRNGs, intended to bring back the feeling of 'personal luck' that some gamers feel when rolling their own dice.}
|
13
|
+
spec.homepage = "https://github.com/neilslater/pool_of_entropy"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_development_dependency "yard", ">= 0.8.7.2"
|
22
|
+
spec.add_development_dependency "bundler", ">= 1.3"
|
23
|
+
spec.add_development_dependency "rspec", ">= 2.13.0"
|
24
|
+
spec.add_development_dependency "rake", ">= 1.9.1"
|
25
|
+
spec.add_development_dependency "coveralls", ">= 0.6.7"
|
26
|
+
end
|
@@ -0,0 +1,541 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe PoolOfEntropy::CorePRNG do
|
4
|
+
describe "class methods" do
|
5
|
+
|
6
|
+
describe "#new" do
|
7
|
+
it "should instantiate a default object" do
|
8
|
+
prng = PoolOfEntropy::CorePRNG.new
|
9
|
+
prng.should be_a PoolOfEntropy::CorePRNG
|
10
|
+
prng.size.should == 1
|
11
|
+
end
|
12
|
+
|
13
|
+
it "should allow setting number of blocks in pool" do
|
14
|
+
prng = PoolOfEntropy::CorePRNG.new( 10 )
|
15
|
+
prng.should be_a PoolOfEntropy::CorePRNG
|
16
|
+
prng.size.should == 10
|
17
|
+
end
|
18
|
+
|
19
|
+
it "should fail with incorrect block number" do
|
20
|
+
expect { PoolOfEntropy::CorePRNG.new( -43 ) }.to raise_error ArgumentError
|
21
|
+
expect { PoolOfEntropy::CorePRNG.new( -1 ) }.to raise_error ArgumentError
|
22
|
+
expect { PoolOfEntropy::CorePRNG.new( 0 ) }.to raise_error ArgumentError
|
23
|
+
expect { PoolOfEntropy::CorePRNG.new( 257 ) }.to raise_error ArgumentError
|
24
|
+
expect { PoolOfEntropy::CorePRNG.new( 1000) }.to raise_error ArgumentError
|
25
|
+
expect { PoolOfEntropy::CorePRNG.new( nil ) }.to raise_error TypeError
|
26
|
+
expect { PoolOfEntropy::CorePRNG.new( '' ) }.to raise_error ArgumentError
|
27
|
+
expect { PoolOfEntropy::CorePRNG.new( :foo => 2 ) }.to raise_error TypeError
|
28
|
+
end
|
29
|
+
|
30
|
+
it "should allow setting internal state" do
|
31
|
+
prng = PoolOfEntropy::CorePRNG.new( 1, "\x0" * 64 )
|
32
|
+
prng.should be_a PoolOfEntropy::CorePRNG
|
33
|
+
prng.size.should == 1
|
34
|
+
prng.state.should == "\x0" * 64
|
35
|
+
end
|
36
|
+
|
37
|
+
it "should fail with bad state data" do
|
38
|
+
expect { PoolOfEntropy::CorePRNG.new( 1, '' ) }.to raise_error ArgumentError
|
39
|
+
expect { PoolOfEntropy::CorePRNG.new( 1, "\x0" * 63 ) }.to raise_error ArgumentError
|
40
|
+
expect { PoolOfEntropy::CorePRNG.new( 1, "\x0" * 200 ) }.to raise_error ArgumentError
|
41
|
+
expect { PoolOfEntropy::CorePRNG.new( 2, "\x0" * 64 ) }.to raise_error ArgumentError
|
42
|
+
end
|
43
|
+
|
44
|
+
it "should allow setting mix_block_id" do
|
45
|
+
prng = PoolOfEntropy::CorePRNG.new( 3, "\x12" * 192, 1 )
|
46
|
+
prng.should be_a PoolOfEntropy::CorePRNG
|
47
|
+
prng.size.should == 3
|
48
|
+
prng.state.should == "\x12" * 192
|
49
|
+
prng.mix_block_id.should == 1
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
describe "instance methods" do
|
55
|
+
|
56
|
+
describe "#clone" do
|
57
|
+
it "should copy all attributes" do
|
58
|
+
prng_orig = PoolOfEntropy::CorePRNG.new
|
59
|
+
prng_copy = prng_orig.clone
|
60
|
+
|
61
|
+
prng_copy.size.should == prng_orig.size
|
62
|
+
prng_copy.state.should == prng_orig.state
|
63
|
+
prng_copy.mix_block_id.should == prng_orig.mix_block_id
|
64
|
+
end
|
65
|
+
|
66
|
+
it "should deep clone the internal state string" do
|
67
|
+
prng_orig = PoolOfEntropy::CorePRNG.new
|
68
|
+
prng_copy = prng_orig.clone
|
69
|
+
prng_copy.state.should_not be prng_orig.state
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
describe "#update" do
|
74
|
+
it "should change the internal state" do
|
75
|
+
prng = PoolOfEntropy::CorePRNG.new
|
76
|
+
init_state = prng.state.clone
|
77
|
+
prng.update( 'boo' )
|
78
|
+
prng.state.should_not == init_state
|
79
|
+
end
|
80
|
+
|
81
|
+
it "should not change the length of the internal state" do
|
82
|
+
prng = PoolOfEntropy::CorePRNG.new
|
83
|
+
prng.update( 'boo' )
|
84
|
+
prng.state.length.should == 64
|
85
|
+
prng.update( 'boowgkjwrhqgioueqrhgiue2hguirhqwiughreuioghreuifhqwoifhr3iufghfwrgrwgetdfwd' )
|
86
|
+
prng.state.length.should == 64
|
87
|
+
|
88
|
+
prng = PoolOfEntropy::CorePRNG.new( 5 )
|
89
|
+
prng.update( 'boo' )
|
90
|
+
prng.state.length.should == 5 * 64
|
91
|
+
prng.update( 'getdfwd' * 1000 )
|
92
|
+
prng.state.length.should == 5 * 64
|
93
|
+
prng.update( 'boefewfweo' )
|
94
|
+
prng.state.length.should == 5 * 64
|
95
|
+
prng.update( 'geefewftdfwd' * 1000 )
|
96
|
+
prng.state.length.should == 5 * 64
|
97
|
+
end
|
98
|
+
|
99
|
+
it "should only change 64 bytes of state at a time" do
|
100
|
+
prng = PoolOfEntropy::CorePRNG.new( 5 )
|
101
|
+
init_state = prng.state.clone
|
102
|
+
|
103
|
+
prng.update( 'boo' )
|
104
|
+
prng.state[64,4*64].should == init_state[64,4*64]
|
105
|
+
next_state = prng.state.clone
|
106
|
+
|
107
|
+
prng.update( 'getdfwd' * 1000 )
|
108
|
+
prng.state[128,3*64].should == init_state[128,3*64]
|
109
|
+
prng.state[0,1*64].should == next_state[0,1*64]
|
110
|
+
next_state = prng.state.clone
|
111
|
+
|
112
|
+
prng.update( 'boefewfweo' )
|
113
|
+
prng.state[192,2*64].should == init_state[192,2*64]
|
114
|
+
prng.state[0,2*64].should == next_state[0,2*64]
|
115
|
+
next_state = prng.state.clone
|
116
|
+
|
117
|
+
prng.update( 'geefewftdfwd' * 1000 )
|
118
|
+
prng.state[256,1*64].should == init_state[256,1*64]
|
119
|
+
prng.state[0,3*64].should == next_state[0,3*64]
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
instance01 = PoolOfEntropy::CorePRNG.new
|
124
|
+
instance01.update( 'QWertyuiopp' )
|
125
|
+
instance01.update( 'Asdfghjkjl' )
|
126
|
+
instance01.update( 'Zxcvbnm' )
|
127
|
+
|
128
|
+
pool_types = [
|
129
|
+
[
|
130
|
+
'default instance',
|
131
|
+
PoolOfEntropy::CorePRNG.new
|
132
|
+
],
|
133
|
+
[
|
134
|
+
'instance with 2KB pool size',
|
135
|
+
PoolOfEntropy::CorePRNG.new( 32 )
|
136
|
+
],
|
137
|
+
[
|
138
|
+
'instance with initial state all 0',
|
139
|
+
PoolOfEntropy::CorePRNG.new( 1, "\x0" * 64 )
|
140
|
+
],
|
141
|
+
[
|
142
|
+
'instance with fixed initial state',
|
143
|
+
PoolOfEntropy::CorePRNG.new( 5, "fiver" * 64, 3 )
|
144
|
+
],
|
145
|
+
[
|
146
|
+
'instance cloned from 2KB instance',
|
147
|
+
PoolOfEntropy::CorePRNG.new( 32 ).clone
|
148
|
+
],
|
149
|
+
[
|
150
|
+
'instance that has been updated with user data',
|
151
|
+
instance01
|
152
|
+
],
|
153
|
+
]
|
154
|
+
|
155
|
+
# NB "probability" and "randomness" tests in the following block are very light, just
|
156
|
+
# intended to capture high-level failures in logic. See DIEHARDER_TEST.md for thorough
|
157
|
+
# checks on statistical randomness of PoolOfEntropy::CorePRNG
|
158
|
+
pool_types.each do |prng_name, prng|
|
159
|
+
|
160
|
+
context "using #{prng_name}" do
|
161
|
+
|
162
|
+
describe "#read_bytes" do
|
163
|
+
it "always returns a 16 byte string" do
|
164
|
+
100.times { prng.read_bytes.length.should == 16 }
|
165
|
+
end
|
166
|
+
|
167
|
+
it "has a high probability of returning a different string each time" do
|
168
|
+
Set[ *(1..100).map {prng.read_bytes} ].size.should == 100
|
169
|
+
end
|
170
|
+
|
171
|
+
describe "with adjustments" do
|
172
|
+
it "always returns a 16 byte string" do
|
173
|
+
100.times { prng.read_bytes('654321').length.should == 16 }
|
174
|
+
end
|
175
|
+
|
176
|
+
it "has a high probability of returning a different string each time" do
|
177
|
+
Set[ *(1..100).map {prng.read_bytes('654321')} ].size.should == 100
|
178
|
+
end
|
179
|
+
|
180
|
+
it "changes output, but does not include adjustments in changes to state" do
|
181
|
+
prng_copy = prng.clone
|
182
|
+
10.times do
|
183
|
+
prng.read_bytes('Hello!').should == prng_copy.read_bytes('Hello!')
|
184
|
+
prng.state.should == prng_copy.state
|
185
|
+
prng.read_bytes('Hello!').should_not == prng_copy.read_bytes('Goodbye!')
|
186
|
+
prng.state.should == prng_copy.state
|
187
|
+
prng.read_bytes.should_not == prng_copy.read_bytes('Goodbye!')
|
188
|
+
prng.state.should == prng_copy.state
|
189
|
+
prng.read_bytes('Hello!').should_not == prng_copy.read_bytes
|
190
|
+
prng.state.should == prng_copy.state
|
191
|
+
prng.read_bytes('Hello','Goodbye').should_not == prng_copy.read_bytes
|
192
|
+
prng.state.should == prng_copy.state
|
193
|
+
# Verify that output remains same for next rolls
|
194
|
+
prng.read_bytes('Foobar','Wibble').should == prng_copy.read_bytes('Foobar','Wibble')
|
195
|
+
prng.state.should == prng_copy.state
|
196
|
+
prng.read_bytes.should == prng_copy.read_bytes
|
197
|
+
prng.state.should == prng_copy.state
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
describe "#read_hex" do
|
204
|
+
it "always returns a 32 digit hex string" do
|
205
|
+
100.times do
|
206
|
+
hex = prng.read_hex
|
207
|
+
hex.length.should == 32
|
208
|
+
hex.should match /\A[0-9a-f]{32}\z/
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
it "has a high probability of returning a different string each time" do
|
213
|
+
Set[ *(1..100).map {prng.read_hex} ].size.should == 100
|
214
|
+
end
|
215
|
+
|
216
|
+
describe "with adjustments" do
|
217
|
+
it "always returns a 32 digit hex string" do
|
218
|
+
100.times do
|
219
|
+
hex = prng.read_hex('QWertyeu')
|
220
|
+
hex.length.should == 32
|
221
|
+
hex.should match /\A[0-9a-f]{32}\z/
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
it "has a high probability of returning a different string each time" do
|
226
|
+
Set[ *(1..100).map {prng.read_hex('654321')} ].size.should == 100
|
227
|
+
end
|
228
|
+
|
229
|
+
it "changes output, but does not include adjustments in changes to state" do
|
230
|
+
prng_copy = prng.clone
|
231
|
+
10.times do
|
232
|
+
prng.read_hex('Hello!').should == prng_copy.read_hex('Hello!')
|
233
|
+
prng.state.should == prng_copy.state
|
234
|
+
prng.read_hex('Hello!').should_not == prng_copy.read_hex('Goodbye!')
|
235
|
+
prng.state.should == prng_copy.state
|
236
|
+
prng.read_hex.should_not == prng_copy.read_hex('Goodbye!')
|
237
|
+
prng.state.should == prng_copy.state
|
238
|
+
prng.read_hex('Hello!').should_not == prng_copy.read_hex
|
239
|
+
prng.state.should == prng_copy.state
|
240
|
+
prng.read_hex('Hello','Goodbye').should_not == prng_copy.read_hex
|
241
|
+
prng.state.should == prng_copy.state
|
242
|
+
# Verify that output remains same for next rolls
|
243
|
+
prng.read_hex('Foobar','Wibble').should == prng_copy.read_hex('Foobar','Wibble')
|
244
|
+
prng.state.should == prng_copy.state
|
245
|
+
prng.read_hex.should == prng_copy.read_hex
|
246
|
+
prng.state.should == prng_copy.state
|
247
|
+
end
|
248
|
+
end
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
describe "#read_bignum" do
|
253
|
+
it "always returns a 128-bit unsigned integer" do
|
254
|
+
100.times do
|
255
|
+
num = prng.read_bignum
|
256
|
+
num.should >= 0
|
257
|
+
num.should < 2**128
|
258
|
+
end
|
259
|
+
end
|
260
|
+
|
261
|
+
it "has a high probability of returning a different number each time" do
|
262
|
+
Set[ *(1..100).map {prng.read_bignum} ].size.should == 100
|
263
|
+
end
|
264
|
+
|
265
|
+
describe "with adjustments" do
|
266
|
+
it "always returns a 128-bit unsigned integer" do
|
267
|
+
100.times do
|
268
|
+
num = prng.read_bignum( 'Biggest' )
|
269
|
+
num.should >= 0
|
270
|
+
num.should < 2**128
|
271
|
+
end
|
272
|
+
end
|
273
|
+
|
274
|
+
it "has a high probability of returning a different number each time" do
|
275
|
+
Set[ *(1..100).map {prng.read_bignum('654321')} ].size.should == 100
|
276
|
+
end
|
277
|
+
|
278
|
+
it "changes output, but does not include adjustments in changes to state" do
|
279
|
+
prng_copy = prng.clone
|
280
|
+
10.times do
|
281
|
+
prng.read_bignum('Hello!').should == prng_copy.read_bignum('Hello!')
|
282
|
+
prng.state.should == prng_copy.state
|
283
|
+
prng.read_bignum('Hello!').should_not == prng_copy.read_bignum('Goodbye!')
|
284
|
+
prng.state.should == prng_copy.state
|
285
|
+
prng.read_bignum.should_not == prng_copy.read_bignum('Goodbye!')
|
286
|
+
prng.state.should == prng_copy.state
|
287
|
+
prng.read_bignum('Hello!').should_not == prng_copy.read_bignum
|
288
|
+
prng.state.should == prng_copy.state
|
289
|
+
prng.read_bignum('Hello','Goodbye').should_not == prng_copy.read_bignum
|
290
|
+
prng.state.should == prng_copy.state
|
291
|
+
# Verify that output remains same for next rolls
|
292
|
+
prng.read_bignum('Foobar','Wibble').should == prng_copy.read_bignum('Foobar','Wibble')
|
293
|
+
prng.state.should == prng_copy.state
|
294
|
+
prng.read_bignum.should == prng_copy.read_bignum
|
295
|
+
prng.state.should == prng_copy.state
|
296
|
+
end
|
297
|
+
end
|
298
|
+
end
|
299
|
+
end
|
300
|
+
|
301
|
+
describe "#read_float" do
|
302
|
+
it "always returns a Float between 0.0 (inclusive) and 1.0 (exclusive)" do
|
303
|
+
100.times do
|
304
|
+
num = prng.read_float
|
305
|
+
num.should be_a Float
|
306
|
+
num.should >= 0.0
|
307
|
+
num.should < 1.0
|
308
|
+
end
|
309
|
+
end
|
310
|
+
|
311
|
+
it "has a high probability of returning a different Float each time" do
|
312
|
+
Set[ *(1..100).map {prng.read_float} ].size.should == 100
|
313
|
+
end
|
314
|
+
|
315
|
+
describe "with adjustments" do
|
316
|
+
it "always returns a Float between 0.0 (inclusive) and 1.0 (exclusive)" do
|
317
|
+
100.times do
|
318
|
+
num = prng.read_float('Boom')
|
319
|
+
num.should be_a Float
|
320
|
+
num.should >= 0.0
|
321
|
+
num.should < 1.0
|
322
|
+
end
|
323
|
+
end
|
324
|
+
|
325
|
+
it "has a high probability of returning a different Float each time" do
|
326
|
+
Set[ *(1..100).map {prng.read_float('654321')} ].size.should == 100
|
327
|
+
end
|
328
|
+
|
329
|
+
it "changes output, but does not include adjustments in changes to state" do
|
330
|
+
prng_copy = prng.clone
|
331
|
+
10.times do
|
332
|
+
prng.read_float('Hello!').should == prng_copy.read_float('Hello!')
|
333
|
+
prng.state.should == prng_copy.state
|
334
|
+
prng.read_float('Hello!').should_not == prng_copy.read_float('Goodbye!')
|
335
|
+
prng.state.should == prng_copy.state
|
336
|
+
prng.read_float.should_not == prng_copy.read_float('Goodbye!')
|
337
|
+
prng.state.should == prng_copy.state
|
338
|
+
prng.read_float('Hello!').should_not == prng_copy.read_float
|
339
|
+
prng.state.should == prng_copy.state
|
340
|
+
prng.read_float('Hello','Goodbye').should_not == prng_copy.read_float
|
341
|
+
prng.state.should == prng_copy.state
|
342
|
+
# Verify that output remains same for next rolls
|
343
|
+
prng.read_float('Foobar','Wibble').should == prng_copy.read_float('Foobar','Wibble')
|
344
|
+
prng.state.should == prng_copy.state
|
345
|
+
prng.read_float.should == prng_copy.read_float
|
346
|
+
prng.state.should == prng_copy.state
|
347
|
+
end
|
348
|
+
end
|
349
|
+
end
|
350
|
+
end
|
351
|
+
|
352
|
+
describe "#generate_integer" do
|
353
|
+
it "always returns an integer between 0 (inclusive) and supplied top (exclusive)" do
|
354
|
+
100.times do
|
355
|
+
num = prng.generate_integer( 10 )
|
356
|
+
num.should be_a Fixnum
|
357
|
+
num.should >= 0
|
358
|
+
num.should < 10
|
359
|
+
end
|
360
|
+
|
361
|
+
100.times do
|
362
|
+
num = prng.generate_integer( 100 )
|
363
|
+
num.should be_a Fixnum
|
364
|
+
num.should >= 0
|
365
|
+
num.should < 100
|
366
|
+
end
|
367
|
+
|
368
|
+
100.times do
|
369
|
+
num = prng.generate_integer( 1000 )
|
370
|
+
num.should be_a Fixnum
|
371
|
+
num.should >= 0
|
372
|
+
num.should < 1000
|
373
|
+
end
|
374
|
+
|
375
|
+
100.times do
|
376
|
+
num = prng.generate_integer( 647218456 )
|
377
|
+
num.should be_a Fixnum
|
378
|
+
num.should >= 0
|
379
|
+
num.should < 647218456
|
380
|
+
end
|
381
|
+
end
|
382
|
+
|
383
|
+
it "can generate integers larger than 2**128" do
|
384
|
+
results = (0..100).map do
|
385
|
+
num = prng.generate_integer( 2 ** 1024 )
|
386
|
+
num.should >= 0
|
387
|
+
num.should < 2 ** 1024
|
388
|
+
num
|
389
|
+
end
|
390
|
+
results.select { |n| n > 2 ** 1020 }.count.should > 50
|
391
|
+
end
|
392
|
+
|
393
|
+
it "with a large enough value for top, has a high probability of returning a different integer each time" do
|
394
|
+
Set[ *(1..100).map {prng.generate_integer( 2**75 - 7 )} ].size.should == 100
|
395
|
+
end
|
396
|
+
|
397
|
+
it "covers a distribution 0...top" do
|
398
|
+
Set[ *(1..100).map {prng.generate_integer(10)} ].size.should == 10
|
399
|
+
end
|
400
|
+
|
401
|
+
describe "with adjustments" do
|
402
|
+
it "always returns an integer between 0 (inclusive) and supplied top (exclusive)" do
|
403
|
+
100.times do
|
404
|
+
num = prng.generate_integer( 10, 'jkffwe' )
|
405
|
+
num.should be_a Fixnum
|
406
|
+
num.should >= 0
|
407
|
+
num.should < 10
|
408
|
+
end
|
409
|
+
|
410
|
+
100.times do
|
411
|
+
num = prng.generate_integer( 100, 'jkffweefewg' )
|
412
|
+
num.should be_a Fixnum
|
413
|
+
num.should >= 0
|
414
|
+
num.should < 100
|
415
|
+
end
|
416
|
+
|
417
|
+
100.times do
|
418
|
+
num = prng.generate_integer( 1000, 'jkffweefewg', 'efhwjkfgw' )
|
419
|
+
num.should be_a Fixnum
|
420
|
+
num.should >= 0
|
421
|
+
num.should < 1000
|
422
|
+
end
|
423
|
+
|
424
|
+
100.times do
|
425
|
+
num = prng.generate_integer( 647218456, 'j*****g', 'efhwjkfgw' )
|
426
|
+
num.should be_a Fixnum
|
427
|
+
num.should >= 0
|
428
|
+
num.should < 647218456
|
429
|
+
end
|
430
|
+
end
|
431
|
+
|
432
|
+
it "with a large enough value for top, has a high probability of returning a different integer each time" do
|
433
|
+
Set[ *(1..100).map {prng.generate_integer( 2**80 - 5, '654321')} ].size.should == 100
|
434
|
+
end
|
435
|
+
|
436
|
+
it "changes output, but does not include adjustments in changes to state" do
|
437
|
+
prng_copy = prng.clone
|
438
|
+
big = 2 ** 64 - 3
|
439
|
+
small = 20
|
440
|
+
10.times do
|
441
|
+
prng.generate_integer( small, 'Hello!').should == prng_copy.generate_integer( small, 'Hello!')
|
442
|
+
prng.state.should == prng_copy.state
|
443
|
+
prng.generate_integer( big, 'Hello!').should_not == prng_copy.generate_integer( big, 'Goodbye!')
|
444
|
+
prng.state.should == prng_copy.state
|
445
|
+
prng.generate_integer( big ).should_not == prng_copy.generate_integer( big, 'Goodbye!')
|
446
|
+
prng.state.should == prng_copy.state
|
447
|
+
prng.generate_integer( big, 'Hello!').should_not == prng_copy.generate_integer( big )
|
448
|
+
prng.state.should == prng_copy.state
|
449
|
+
prng.generate_integer( big, 'Hello','Goodbye').should_not == prng_copy.generate_integer( big )
|
450
|
+
prng.state.should == prng_copy.state
|
451
|
+
# Verify that output remains same for next rolls
|
452
|
+
prng.generate_integer( small, 'Foobar','Wibble').should == prng_copy.generate_integer( small, 'Foobar','Wibble')
|
453
|
+
prng.state.should == prng_copy.state
|
454
|
+
prng.generate_integer( big ).should == prng_copy.generate_integer( big )
|
455
|
+
prng.state.should == prng_copy.state
|
456
|
+
end
|
457
|
+
end
|
458
|
+
end
|
459
|
+
end
|
460
|
+
|
461
|
+
|
462
|
+
describe "all read methods" do
|
463
|
+
|
464
|
+
it "generate higher and lower values in sync" do
|
465
|
+
prngs = (0..4).map { prng.clone }
|
466
|
+
results = (1..20).map do
|
467
|
+
Hash[
|
468
|
+
:bytes => prngs[0].read_bytes,
|
469
|
+
:hex => prngs[1].read_hex,
|
470
|
+
:float => prngs[2].read_float,
|
471
|
+
:num => prngs[3].read_bignum,
|
472
|
+
:int => prngs[4].generate_integer( 1_000_000_000 ),
|
473
|
+
]
|
474
|
+
end
|
475
|
+
results.sort_by { |h| h[:int] }.should == results.sort_by { |h| h[:hex] }
|
476
|
+
results.sort_by { |h| h[:float] }.should == results.sort_by { |h| h[:hex] }
|
477
|
+
results.sort_by { |h| h[:num] }.should == results.sort_by { |h| h[:bytes] }
|
478
|
+
results.sort_by { |h| h[:float] }.should == results.sort_by { |h| h[:int] }
|
479
|
+
end
|
480
|
+
end
|
481
|
+
|
482
|
+
end
|
483
|
+
end
|
484
|
+
|
485
|
+
describe "using predictable sequences" do
|
486
|
+
it "generates expected hex strings from simple zeroed pool" do
|
487
|
+
prng = PoolOfEntropy::CorePRNG.new( 1, "\x0" * 64 )
|
488
|
+
(0..24).map { prng.read_hex }.should == [ "da0cd77eb1c84458ddcc91e36b9dcb35",
|
489
|
+
"498ec24d1126440047eed396d836b5e1", "0b90df55c5e7c1513b072367eae4a4ce",
|
490
|
+
"f12b5b54b5594e785bbb9a4ac50ccec8", "d506bd4e201a00dc30499bd8e59a30d8",
|
491
|
+
"2557893cf995fe43bd00721fce6ab16a", "41ee50244cdd02334bafc3e9d8f564d9",
|
492
|
+
"8156c7c7bcfd856cb9d1012243cfc662", "1116f840e2aee924bd2c7d722c602635",
|
493
|
+
"96cf31967465b83f5ef3476c60afe20a", "f041f6df72aa2eab7394f08e83d52a0c",
|
494
|
+
"5ccbb30077f2433bd765ddc86840a880", "6a4339fd5d445024048ea8f91a3e02fd",
|
495
|
+
"707e9499b9f0e9e906a0c0ddd0530803", "553704d95703284df323f0aa4244cf81",
|
496
|
+
"cb1522ce0bdf2504fdd62df7416be73e", "05c4932cb9d3a7c0675b0228826e661a",
|
497
|
+
"59606f23e34e726a7912dacfde533d97", "188cf17dde6947264ce05f8274874ffa",
|
498
|
+
"24c184b56453891657953f557635b742", "a5f969e50228b2ee0f38cc37c5033541",
|
499
|
+
"f1de0e5d149cc5a7f0e38c0ee501d7bc", "5ca3a6beb0b810568be83ab2179eb550",
|
500
|
+
"756fb9c58277eb8c6092142224caecf4", "0ed9505eadb3def60ec42051f0bf15ef" ]
|
501
|
+
end
|
502
|
+
|
503
|
+
it "generates expected hex strings from simple zeroed pool and an adjustment" do
|
504
|
+
prng = PoolOfEntropy::CorePRNG.new( 1, "\x0" * 64 )
|
505
|
+
(0..24).map { prng.read_hex( 'bananas' ) }.should == [
|
506
|
+
"fed9de9a612f4157ebb49582ca557a50", "a7adc2374a4df2ed67846ac09a3b6645",
|
507
|
+
"cbdf8bbbe6145751fe14004719915160", "8cd84be95376f72918c305bdea43e36d",
|
508
|
+
"9e08e80ff40c942f0cf4d479b5378fa0", "54d80c5330873f9733a0adef0197220f",
|
509
|
+
"2abe07bd85d2066a624dd3630e59730a", "88b6a697fe74aeb8ec83845e103b7b63",
|
510
|
+
"9c6c9d613855f6535adb419cc564fd10", "23f9b778b254035a0e4219423a52da77",
|
511
|
+
"4cd50c14ee17fa29c8b1f209432a36b3", "2b5646b164d863de716f67adef653859",
|
512
|
+
"fd81d0bbbd2828ecdfcba0b486ef786c", "08e6594fc277ff7fafbf37475ecadbf6",
|
513
|
+
"5e2333ecc800eb9a06e347924ed42e94", "8a14e48013028b2c9f174a07ddd5ef49",
|
514
|
+
"e9a5c331f54b155f570c2cf2bcd37209", "1ce84279195a5b23ffeb3063edbfab21",
|
515
|
+
"7422e19e58f2fcf20805f601266bf676", "8df0226465b74d830360ea8609d181bf",
|
516
|
+
"3e5d5e8cf8ad032fb4005826bdf7bb94", "65e3f9ab28c26bead6647c21bcc6245e",
|
517
|
+
"8ed5d8892d464ebeaa8dcbcef0968936", "2f000bc1ab14d914196cc1d5055db189",
|
518
|
+
"8e9c4b8152f14e47ac31f25a80765ebf" ]
|
519
|
+
end
|
520
|
+
|
521
|
+
it "generates expected hex strings from simple zeroed pool and alternating adjustments" do
|
522
|
+
prng = PoolOfEntropy::CorePRNG.new( 1, "\x0" * 64 )
|
523
|
+
(0..24).map { |x| x.odd? ? prng.read_hex( 'bananas', x.to_s ) : prng.read_hex }.should == [
|
524
|
+
"da0cd77eb1c84458ddcc91e36b9dcb35", "de4b20a0560263090c6fe11ebba6256e",
|
525
|
+
"0b90df55c5e7c1513b072367eae4a4ce", "0c92d534160cb268cb3282a9dd3f1efe",
|
526
|
+
"d506bd4e201a00dc30499bd8e59a30d8", "089f799236a9cf64f22f1deafbe28214",
|
527
|
+
"41ee50244cdd02334bafc3e9d8f564d9", "84477b9a3d51bb72868ccb85bea71cad",
|
528
|
+
"1116f840e2aee924bd2c7d722c602635", "469e4bb2f38719c7f85d79c8900a8fa8",
|
529
|
+
"f041f6df72aa2eab7394f08e83d52a0c", "e21bf2508b8e1755ae70c74c0d36ec2e",
|
530
|
+
"6a4339fd5d445024048ea8f91a3e02fd", "a94a80c80c9edd95808a67775c2b42d4",
|
531
|
+
"553704d95703284df323f0aa4244cf81", "edd6aea5bda7b2d9c494067c5cdbe410",
|
532
|
+
"05c4932cb9d3a7c0675b0228826e661a", "cf81635ca2fa910fd11cf45508dc658b",
|
533
|
+
"188cf17dde6947264ce05f8274874ffa", "bb4aaeb98e13400c1216c674a872ba93",
|
534
|
+
"a5f969e50228b2ee0f38cc37c5033541", "1a795355f979575247b3fa9c92d97917",
|
535
|
+
"5ca3a6beb0b810568be83ab2179eb550", "1a98419cca1dd1b370ce8a4a2ab721b7",
|
536
|
+
"0ed9505eadb3def60ec42051f0bf15ef" ]
|
537
|
+
end
|
538
|
+
end
|
539
|
+
end
|
540
|
+
|
541
|
+
end
|