pool_of_entropy 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|