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  }