github.com/onflow/flow-go/crypto@v0.24.8/random/rand_test.go (about) 1 package random 2 3 import ( 4 "bytes" 5 mrand "math/rand" 6 "testing" 7 "time" 8 9 "github.com/stretchr/testify/assert" 10 "github.com/stretchr/testify/require" 11 "golang.org/x/crypto/chacha20" 12 ) 13 14 // sanity check for the underlying implementation of Chacha20 15 // to make sure the implementation is compliant the RFC 7539. 16 func TestChacha20Compliance(t *testing.T) { 17 18 t.Run("key and nonce length", func(t *testing.T) { 19 20 assert.Equal(t, Chacha20SeedLen, 32) 21 assert.Equal(t, Chacha20CustomizerMaxLen, 12) 22 }) 23 24 t.Run("RFC test vector", func(t *testing.T) { 25 26 key := []byte{ 27 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 28 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 29 } 30 nonce := []byte{0, 0, 0, 0, 0, 0, 0, 0x4a, 0, 0, 0, 0} 31 counter := uint32(1) 32 plaintext := []byte{ 33 0x4c, 0x61, 0x64, 0x69, 0x65, 0x73, 0x20, 0x61, 0x6e, 0x64, 0x20, 0x47, 0x65, 0x6e, 0x74, 0x6c, 34 0x65, 0x6d, 0x65, 0x6e, 0x20, 0x6f, 0x66, 0x20, 0x74, 0x68, 0x65, 0x20, 0x63, 0x6c, 0x61, 0x73, 35 0x73, 0x20, 0x6f, 0x66, 0x20, 0x27, 0x39, 0x39, 0x3a, 0x20, 0x49, 0x66, 0x20, 0x49, 0x20, 0x63, 36 0x6f, 0x75, 0x6c, 0x64, 0x20, 0x6f, 0x66, 0x66, 0x65, 0x72, 0x20, 0x79, 0x6f, 0x75, 0x20, 0x6f, 37 0x6e, 0x6c, 0x79, 0x20, 0x6f, 0x6e, 0x65, 0x20, 0x74, 0x69, 0x70, 0x20, 0x66, 0x6f, 0x72, 0x20, 38 0x74, 0x68, 0x65, 0x20, 0x66, 0x75, 0x74, 0x75, 0x72, 0x65, 0x2c, 0x20, 0x73, 0x75, 0x6e, 0x73, 39 0x63, 0x72, 0x65, 0x65, 0x6e, 0x20, 0x77, 0x6f, 0x75, 0x6c, 0x64, 0x20, 0x62, 0x65, 0x20, 0x69, 40 } 41 ciphertext := []byte{ 42 0x6e, 0x2e, 0x35, 0x9a, 0x25, 0x68, 0xf9, 0x80, 0x41, 0xba, 0x07, 0x28, 0xdd, 0x0d, 0x69, 0x81, 43 0xe9, 0x7e, 0x7a, 0xec, 0x1d, 0x43, 0x60, 0xc2, 0x0a, 0x27, 0xaf, 0xcc, 0xfd, 0x9f, 0xae, 0x0b, 44 0xf9, 0x1b, 0x65, 0xc5, 0x52, 0x47, 0x33, 0xab, 0x8f, 0x59, 0x3d, 0xab, 0xcd, 0x62, 0xb3, 0x57, 45 0x16, 0x39, 0xd6, 0x24, 0xe6, 0x51, 0x52, 0xab, 0x8f, 0x53, 0x0c, 0x35, 0x9f, 0x08, 0x61, 0xd8, 46 0x07, 0xca, 0x0d, 0xbf, 0x50, 0x0d, 0x6a, 0x61, 0x56, 0xa3, 0x8e, 0x08, 0x8a, 0x22, 0xb6, 0x5e, 47 0x52, 0xbc, 0x51, 0x4d, 0x16, 0xcc, 0xf8, 0x06, 0x81, 0x8c, 0xe9, 0x1a, 0xb7, 0x79, 0x37, 0x36, 48 0x5a, 0xf9, 0x0b, 0xbf, 0x74, 0xa3, 0x5b, 0xe6, 0xb4, 0x0b, 0x8e, 0xed, 0xf2, 0x78, 0x5e, 0x42, 49 } 50 51 chacha, err := chacha20.NewUnauthenticatedCipher(key, nonce) 52 require.NoError(t, err) 53 chacha.SetCounter(counter) 54 chacha.XORKeyStream(plaintext, plaintext) 55 assert.Equal(t, plaintext, ciphertext) 56 57 }) 58 59 t.Run("invalid constructor inputs", func(t *testing.T) { 60 seed := make([]byte, Chacha20SeedLen+1) 61 customizer := make([]byte, Chacha20CustomizerMaxLen+1) 62 63 // long seed 64 _, err := NewChacha20PRG(seed, customizer[:Chacha20CustomizerMaxLen]) 65 assert.Error(t, err) 66 // long nonce 67 _, err = NewChacha20PRG(seed[:Chacha20SeedLen], customizer) 68 assert.Error(t, err) 69 }) 70 71 t.Run("short nonce", func(t *testing.T) { 72 seed := make([]byte, Chacha20SeedLen) 73 customizer := make([]byte, Chacha20CustomizerMaxLen) 74 75 // short nonces should be accepted 76 _, err := NewChacha20PRG(seed, customizer[:Chacha20CustomizerMaxLen-1]) 77 assert.NoError(t, err) 78 _, err = NewChacha20PRG(seed, customizer[:0]) 79 assert.NoError(t, err) 80 }) 81 } 82 83 func getPRG(t *testing.T) *mrand.Rand { 84 random := time.Now().UnixNano() 85 t.Logf("rng seed is %d", random) 86 rng := mrand.New(mrand.NewSource(random)) 87 return rng 88 } 89 90 // The tests are targeting the PRG implementations in the package. 91 // For now, the tests are only used for Chacha20 PRG, but can be ported 92 // to test another PRG implementation. 93 94 // Simple unit testing of UintN using a basic randomness test. 95 // It doesn't perform advanced statistical tests. 96 func TestUintN(t *testing.T) { 97 rand := getPRG(t) 98 seed := make([]byte, Chacha20SeedLen) 99 _, err := rand.Read(seed) 100 require.NoError(t, err) 101 customizer := make([]byte, Chacha20CustomizerMaxLen) 102 _, err = rand.Read(customizer) 103 require.NoError(t, err) 104 105 rng, err := NewChacha20PRG(seed, customizer) 106 require.NoError(t, err) 107 108 t.Run("basic uniformity", func(t *testing.T) { 109 110 maxN := uint64(1000) 111 mod := mrand.Uint64() 112 var n, classWidth uint64 113 if mod < maxN { // `mod` is too small so that we can consider `mod` classes 114 n = mod 115 classWidth = 1 116 } else { // `mod` is big enough so that we can partition [0,mod-1] into `maxN` classes 117 n = maxN 118 mod = (mod / n) * n // adjust `mod` to make sure it is a multiple of n for a more accurate test 119 classWidth = mod / n 120 } 121 122 uintNf := func() (uint64, error) { 123 return uint64(rng.UintN(mod)), nil 124 } 125 BasicDistributionTest(t, n, classWidth, uintNf) 126 127 }) 128 129 t.Run("zero n", func(t *testing.T) { 130 assert.Panics(t, func() { 131 rng.UintN(0) 132 }) 133 }) 134 } 135 136 // Simple unit testing of SubPermutation using a basic randomness test. 137 // It doesn't perform advanced statistical tests. 138 // 139 // SubPermutation tests cover Permutation as well. 140 func TestSubPermutation(t *testing.T) { 141 rand := getPRG(t) 142 143 seed := make([]byte, Chacha20SeedLen) 144 _, err := rand.Read(seed) 145 require.NoError(t, err) 146 customizer := make([]byte, Chacha20CustomizerMaxLen) 147 _, err = rand.Read(customizer) 148 require.NoError(t, err) 149 150 rng, err := NewChacha20PRG(seed, customizer) 151 require.NoError(t, err) 152 153 t.Run("basic randomness", func(t *testing.T) { 154 listSize := 100 155 subsetSize := 20 156 sampleSize := 85000 157 // tests the subset sampling randomness 158 samplingDistribution := make([]float64, listSize) 159 // tests the subset ordering randomness (using a particular element testElement) 160 orderingDistribution := make([]float64, subsetSize) 161 testElement := rand.Intn(listSize) 162 163 for i := 0; i < sampleSize; i++ { 164 shuffledlist, err := rng.SubPermutation(listSize, subsetSize) 165 require.NoError(t, err) 166 require.Equal(t, len(shuffledlist), subsetSize) 167 has := make(map[int]struct{}) 168 for j, e := range shuffledlist { 169 // check for repetition 170 _, ok := has[e] 171 require.False(t, ok, "duplicated item") 172 has[e] = struct{}{} 173 // fill the distribution 174 samplingDistribution[e] += 1.0 175 if e == testElement { 176 orderingDistribution[j] += 1.0 177 } 178 } 179 } 180 EvaluateDistributionUniformity(t, samplingDistribution) 181 EvaluateDistributionUniformity(t, orderingDistribution) 182 }) 183 184 // Evaluate that 185 // - permuting an empty set returns an empty list 186 // - drawing a sample of size zero from a non-empty set returns an empty list 187 t.Run("empty sets", func(t *testing.T) { 188 189 // verify that permuting an empty set returns an empty list 190 res, err := rng.SubPermutation(0, 0) 191 require.NoError(t, err) 192 assert.True(t, len(res) == 0) 193 194 // verify that drawing a sample of size zero from a non-empty set returns an empty list 195 res, err = rng.SubPermutation(10, 0) 196 require.NoError(t, err) 197 assert.True(t, len(res) == 0) 198 }) 199 200 t.Run("negative inputs", func(t *testing.T) { 201 res, err := rng.Permutation(-3) 202 require.Error(t, err) 203 assert.Nil(t, res) 204 205 res, err = rng.SubPermutation(5, -3) 206 require.Error(t, err) 207 assert.Nil(t, res) 208 209 res, err = rng.SubPermutation(-3, 5) 210 require.Error(t, err) 211 assert.Nil(t, res) 212 }) 213 } 214 215 // Simple unit testing of Shuffle using a basic randomness test. 216 // It doesn't perform advanced statistical tests. 217 func TestShuffle(t *testing.T) { 218 rand := getPRG(t) 219 220 seed := make([]byte, Chacha20SeedLen) 221 _, err := rand.Read(seed) 222 require.NoError(t, err) 223 customizer := make([]byte, Chacha20CustomizerMaxLen) 224 _, err = rand.Read(customizer) 225 require.NoError(t, err) 226 227 rng, err := NewChacha20PRG(seed, customizer) 228 require.NoError(t, err) 229 230 t.Run("basic uniformity", func(t *testing.T) { 231 listSize := 100 232 sampleSize := 80000 233 // the distribution of a particular element of the list, testElement 234 distribution := make([]float64, listSize) 235 testElement := rand.Intn(listSize) 236 // Slice to shuffle 237 list := make([]int, 0, listSize) 238 for i := 0; i < listSize; i++ { 239 list = append(list, i) 240 } 241 242 shuffleAndCount := func(t *testing.T) { 243 err = rng.Shuffle(listSize, func(i, j int) { 244 list[i], list[j] = list[j], list[i] 245 }) 246 require.NoError(t, err) 247 has := make(map[int]struct{}) 248 for j, e := range list { 249 // check for repetition 250 _, ok := has[e] 251 require.False(t, ok, "duplicated item") 252 has[e] = struct{}{} 253 // fill the distribution 254 if e == testElement { 255 distribution[j] += 1.0 256 } 257 } 258 } 259 260 t.Run("shuffle a random permutation", func(t *testing.T) { 261 for k := 0; k < sampleSize; k++ { 262 shuffleAndCount(t) 263 } 264 EvaluateDistributionUniformity(t, distribution) 265 }) 266 267 t.Run("shuffle a same permutation", func(t *testing.T) { 268 for k := 0; k < sampleSize; k++ { 269 // reinit the permutation to the same value 270 for i := 0; i < listSize; i++ { 271 list[i] = i 272 } 273 shuffleAndCount(t) 274 } 275 EvaluateDistributionUniformity(t, distribution) 276 }) 277 }) 278 279 t.Run("empty slice", func(t *testing.T) { 280 emptySlice := make([]float64, 0) 281 err = rng.Shuffle(len(emptySlice), func(i, j int) { 282 emptySlice[i], emptySlice[j] = emptySlice[j], emptySlice[i] 283 }) 284 require.NoError(t, err) 285 assert.True(t, len(emptySlice) == 0) 286 }) 287 288 t.Run("negative inputs", func(t *testing.T) { 289 emptySlice := make([]float64, 5) 290 err = rng.Shuffle(-3, func(i, j int) { 291 emptySlice[i], emptySlice[j] = emptySlice[j], emptySlice[i] 292 }) 293 require.Error(t, err) 294 }) 295 } 296 297 func TestSamples(t *testing.T) { 298 rand := getPRG(t) 299 300 seed := make([]byte, Chacha20SeedLen) 301 _, err := rand.Read(seed) 302 require.NoError(t, err) 303 customizer := make([]byte, Chacha20CustomizerMaxLen) 304 _, err = rand.Read(customizer) 305 require.NoError(t, err) 306 307 rng, err := NewChacha20PRG(seed, customizer) 308 require.NoError(t, err) 309 310 t.Run("basic uniformity", func(t *testing.T) { 311 listSize := 100 312 samplesSize := 20 313 sampleSize := 100000 314 // tests the subset sampling randomness 315 samplingDistribution := make([]float64, listSize) 316 // tests the subset ordering randomness (using a particular element testElement) 317 orderingDistribution := make([]float64, samplesSize) 318 testElement := rand.Intn(listSize) 319 // Slice to shuffle 320 list := make([]int, 0, listSize) 321 for i := 0; i < listSize; i++ { 322 list = append(list, i) 323 } 324 325 for i := 0; i < sampleSize; i++ { 326 err = rng.Samples(listSize, samplesSize, func(i, j int) { 327 list[i], list[j] = list[j], list[i] 328 }) 329 require.NoError(t, err) 330 has := make(map[int]struct{}) 331 for j, e := range list[:samplesSize] { 332 // check for repetition 333 _, ok := has[e] 334 require.False(t, ok, "duplicated item") 335 has[e] = struct{}{} 336 // fill the distribution 337 samplingDistribution[e] += 1.0 338 if e == testElement { 339 orderingDistribution[j] += 1.0 340 } 341 } 342 } 343 EvaluateDistributionUniformity(t, samplingDistribution) 344 EvaluateDistributionUniformity(t, orderingDistribution) 345 }) 346 347 t.Run("zero edge cases", func(t *testing.T) { 348 // Sampling from an empty set 349 emptySlice := make([]float64, 0) 350 err = rng.Samples(len(emptySlice), len(emptySlice), func(i, j int) { 351 emptySlice[i], emptySlice[j] = emptySlice[j], emptySlice[i] 352 }) 353 require.NoError(t, err) 354 assert.True(t, len(emptySlice) == 0) 355 356 // drawing a sample of size zero from an non-empty list should leave the original list unmodified 357 constant := []float64{0, 1, 2, 3, 4, 5} 358 fullSlice := constant 359 err = rng.Samples(len(fullSlice), 0, func(i, j int) { // modifies fullSlice in-place 360 emptySlice[i], emptySlice[j] = emptySlice[j], emptySlice[i] 361 }) 362 require.NoError(t, err) 363 assert.Equal(t, constant, fullSlice) 364 }) 365 366 t.Run("negative inputs", func(t *testing.T) { 367 emptySlice := make([]float64, 5) 368 err = rng.Samples(-3, 5, func(i, j int) { 369 emptySlice[i], emptySlice[j] = emptySlice[j], emptySlice[i] 370 }) 371 require.Error(t, err) 372 373 err = rng.Samples(-5, 3, func(i, j int) { 374 emptySlice[i], emptySlice[j] = emptySlice[j], emptySlice[i] 375 }) 376 require.Error(t, err) 377 }) 378 } 379 380 // TestStateRestore tests the serilaization and deserialization functions 381 // Store and Restore 382 func TestStateRestore(t *testing.T) { 383 rand := getPRG(t) 384 385 // generate a seed 386 seed := make([]byte, Chacha20SeedLen) 387 _, err := rand.Read(seed) 388 require.NoError(t, err) 389 customizer := make([]byte, Chacha20CustomizerMaxLen) 390 _, err = rand.Read(customizer) 391 require.NoError(t, err) 392 t.Logf("seed is %x, customizer is %x\n", seed, customizer) 393 394 // create an rng 395 rng, err := NewChacha20PRG(seed, customizer) 396 require.NoError(t, err) 397 398 // evolve the internal state of the rng 399 iterations := rand.Intn(1000) 400 for i := 0; i < iterations; i++ { 401 _ = rng.UintN(1024) 402 } 403 // get the internal state of the rng 404 state := rng.Store() 405 406 // check the state is deterministic 407 state_clone := rng.Store() 408 assert.True(t, bytes.Equal(state, state_clone), "Store is not deterministic") 409 410 // check Store is the Restore reverse function 411 secondRng, err := RestoreChacha20PRG(state) 412 require.NoError(t, err) 413 assert.True(t, bytes.Equal(state, secondRng.Store()), "Store o Restore is not identity") 414 415 // check the 2 PRGs are generating identical outputs 416 iterations = rand.Intn(1000) 417 for i := 0; i < iterations; i++ { 418 rand1 := rng.UintN(1024) 419 rand2 := secondRng.UintN(1024) 420 assert.Equal(t, rand1, rand2, "the 2 rngs are not identical on round %d", i) 421 } 422 }