6 de noviembre de 2017

Testing the random numbers in Racket with Random Sanity

Introduction

A few days ago I was looking at the Random Sanity project. It is a web service where you send a string of numbers supposedly at random, to see how bad is the random number generator. It doesn’t make a very exhaustive check, so it only detects cases where the generator is very bad, or cases in which the string of random numbers has already been sent.

The string consists of 64 bytes encoded in hexadecimal. For example

and the result is "
true" or "false".

The idea is to use it to test the random number generation functions in Racket and see if they pass this test.

Auxiliary constructions

random-bytes

Generates a string of bytes of length n. It is similar to crypto-random-bytes, but it uses pseudorandom numbers instead of cryptographically secure numbers. (Strange: There is no for/bytes.)

(define (random-bytes n)
  (list->bytes
   (for/list ([i (in-range n)])
     (random 256))))

with-random-seed

The seed of the pseudorandom numbers in Racket is initialized with the milliseconds near the beginning of the program. It is sometimes convenient to locally modify the seed of pseudorandom numbers without affecting the pseudorandom numbers of the rest of the program. This macro allows us to evaluate the body, fixing the seed, and after that continue with the normal pseudorandom sequence.

(define-syntax-rule (with-random-seed seed body ...)
  (parameterize ([current-pseudo-random-generator (make-pseudo-random-generator)])
    (random-seed seed)
    body ...))

Let’s try some examples

(displayln (random 10))                   ; ==> maybe 3
(with-random-seed 1234567890
  (displayln (random 10))                 ; ==>        1
  (displayln (random 10)))                ; ==>        5
(displayln (random 10))                   ; ==> maybe 2
(with-random-seed 1234567890
  (displayln (random 10))                 ; ==>        1
  (displayln (random 10)))                ; ==>        5
(displayln (random 10))                   ; ==> maybe 5

The tester

The main function is the tester, which uses a bytes string. The tester formats it and sends it via hppt to Random Sanity. Then it reads the test result and transforms it into #t or #f, which are more idiomatic. The function has very few error checks, for simplicity and for laziness.

#;(require net/http-client
           file/sha1)
 (define (test-random-bytes b)
  (let-values ([(status headers content)
                (http-sendrecv "rest.randomsanity.org"
                               (string-append "/v1/q/"
                                              (bytes->hex-string b)))])
    (let ([text (port->string content)])
      (cond
        [(string=? text "true") #t]
        [(string=? text "false") #f]
        [else (raise-result-error 'test-random-bytes
                                  "true or false"
                                  text)]))))

Some tests

All zeros # "000 ... 000"

It is not a good example of random numbers and obviously the result is #f.

(test-random-bytes (make-bytes 64 0))  ; ==> #f

Pseudo-random numbers with a fixed seed

The first time someone uses one seed the result is #t (it was me, believe me, I saw a #t) but when someone runs the program again with the same seed the sequence of pseudorandom numbers is repeated, so in all subsequent retries the result is #f. If you test another seed, you should be able to see a #t once and then #f. (Using the same seed 1234567890 in other languages will probably get a string of different bytes, so you can probably see another "true" once. More details.)

(with-random-seed 1234567890
  (test-random-bytes (random-bytes 64)))   ; ==> #f (except the first time)

Fixed seed pseudorandom numbers

Racket initializes the seed of random numbers with the milliseconds at some point near the start of the execution of the program. So every time you will get a different sequence, probably never seen before. Unless this article is an absolute success and millions of people try to run the program, it is very unlikely to be so unlucky to get a repeated seed. So you almost certainly will get #t.

(test-random-bytes (random-bytes 64))   ; ==> #t (almost sure)

Cryptographically secure numbers

For some applications it is a risk that someone else guesses the sequence. In the previous example someone could estimate when the program was run and test all the possible values of the milliseconds in a nearby range until finding the match. For these cases you can use crypto-random-bytes that generates cryptographically safe numbers. If there is no problem in the operating system or in Racket it should be (almost) impossible to find a match, and get a result that is not #t. (This is precisely the goal of Random Sanity, to detect that a serious error was accidentally introduced into the random number generator.)

#;(require racket/random)
(test-random-bytes (crypto-random-bytes 64))  ;  ==> #t

The complete program

It is necessary to use some modules, so for convenience I’ll repeat the complete program. (Some parts of the program that don´t use internet connections can be tested in PasteRack.)

#lang racket

(require net/http-client
         file/sha1
         racket/random)

(define (random-bytes n)
  (list->bytes
   (for/list ([i (in-range n)])
    (random 256))))

(define-syntax-rule (with-random-seed seed body ...)
  (parameterize ([current-pseudo-random-generator (make-pseudo-random-generator)])
    (random-seed seed)
    body ...))

(define (test-random-bytes b)
  (let-values ([(status headers content)
                (http-sendrecv "rest.randomsanity.org"
                               (string-append "/v1/q/"
                                              (bytes->hex-string b)))])
    (let ([text (port->string content)])
      (cond
        [(string=? text "true") #t]
        [(string=? text "false") #f]
        [else (raise-result-error 'test-random-bytes
                                  "true or false"
                                  text)]))))

(test-random-bytes (make-bytes 64 0))        ; ==> #f
(with-random-seed 1234567890
  (test-random-bytes (random-bytes 64)))     ; ==> #f
(test-random-bytes (random-bytes 64))        ; ==> #t
(test-random-bytes (crypto-random-bytes 64)) ; ==> #t

Probando los números aleatorios de Racket con Random Sanity

Introducción

Hace unos días estuve mirando el proyecto Random Sanity. Es un servicio web al que uno manda una cadena de números supuestamente al azar, para que vea que tan malo es el generador de números al azar. No hace un chequeo muy exhaustivo, así que sólo detecta casos en que el generador es muy malo, o casos en que la cadena de números al azar ya fue enviada.

La cadena consiste en 64 bytes codificados en hexadecimal. Por ejemplo


y el resultado es "true" o "false".

La idea es usarlo para probar las funciones de generación de números al azar en Racket y ver si pasan este test.

Construcciones auxiliares

random-bytes

Genera una cadena de bytes de largo n. Es parecida a crypto-random-bytes, pero usa números pseudoaleatorios en vez de números criptográficamente seguros. (Extraño: No existe for/bytes.)

(define (random-bytes n)
  (list->bytes
   (for/list ([i (in-range n)])
     (random 256))))

with-random-seed

La semilla de los números pseudoaleatorios en Racket se inicializa con los milisegundos cerca del comienzo del programa. A veces es cómodo modificar localmente la semilla de los números pseudoaleatorios sin afectar a los números pseudoaleatorios del resto del programa. Esta macro permite evaluar el body fijando la semilla y que después todo vuelva a la normalidad.

(define-syntax-rule (with-random-seed seed body ...)
  (parameterize ([current-pseudo-random-generator (make-pseudo-random-generator)])
    (random-seed seed)
    body ...))

Probemos algunos ejemplos

(displayln (random 10))                   ; ==> quizás 3
(with-random-seed 1234567890
  (displayln (random 10))                 ; ==>        1
  (displayln (random 10)))                ; ==>        5
(displayln (random 10))                   ; ==> quizás 2
(with-random-seed 1234567890
  (displayln (random 10))                 ; ==>        1
  (displayln (random 10)))                ; ==>        5
(displayln (random 10))                   ; ==> quizás 5

Testeador

La función principal es el testeador, que usa una cadena de bytes. La formatea y envía por hppt a Random Sanity. Luego lee el resultado del test y lo transforma en #t o #f, que son más idiomáticos. La función tiene muy pocos chequeos de errores, por simplicidad y por pachorra.

#;(require net/http-client
           file/sha1)
 (define (test-random-bytes b)
  (let-values ([(status headers content)
                (http-sendrecv "rest.randomsanity.org"
                               (string-append "/v1/q/"
                                              (bytes->hex-string b)))])
    (let ([text (port->string content)])
      (cond
        [(string=? text "true") #t]
        [(string=? text "false") #f]
        [else (raise-result-error 'test-random-bytes
                                  "true or false"
                                  text)]))))

Algunos test

Todos ceros #"000...000"

No es un buen ejemplo de números aleatorios y obviamente el resultado es #f.

(test-random-bytes (make-bytes 64 0))  ; ==> #f

Números pseudoaleatorios con una semilla fija

La primera vez que alguien usa la semilla da #t (fui yo, créanme que vi un #t) pero al ejecutarlo nuevamente con la misma semilla se repite la secuencia de números pseudoalatorios y entonces todas las veces subsiguientes da #f. Probando otra semilla deberían poder ver una vez #t y después #f. (Probando la misma semilla 1234567890 en otros lenguajes probablemente se obtenga una cadena de bytes diferentes, así que probablemente puedan ver una vez el primer "true". Más detalles)

(with-random-seed 1234567890
  (test-random-bytes (random-bytes 64)))   ; ==> #f (salvo la primera vez)

Números pseudoaleatorios sin semilla fija

Racket inicializa la semilla de números aleatorios con los milisegundos en algún momento cerca del inicio de la ejecución del programa. Así que cada vez se obtiene una secuencia diferente, probablemente nunca vista antes. A menos que este artículo sea un éxito desmesurado y millones de personas se pongan a probar el programa es muy poco probable que al ejecutarlo uno tenga tanta mala suerte que se repita la semilla. Así que casi seguramente uno obtiene #t.

(test-random-bytes (random-bytes 64))   ; ==> #t (casi seguro)

Números criptográficamente seguros

Para algunas aplicaciones es un riesgo que alguien adivine la secuencia. En el ejemplo anterior alguien podría estimar cuándo se ejecutó el programa y probar con todos los posibles valores de los milisegundos en un rango cercano hasta encontrar la coincidencia. Para esos casos se puede usar crypto-random-bytes que genera números criptográficamente seguros. Si no hay ningún problema en el sistema operativo o en Racket debería ser (casi) imposible encontrar una coincidencia y que el resultado no sea #t. (Este es justamente el objetivo de Random Sanity, detectar que involuntariamente se introdujo un error grave en el generador de números aleatorios.)

#;(require racket/random)
(test-random-bytes (crypto-random-bytes 64))  ;  ==> #t

El programa completo

Es necesario usar algunos módulos, así que por comodidad repitamos el programa completo. (Algunas partes del programa que no usan conexiones de internet se pueden probar en PasteRack.)


#lang racket

(require net/http-client
         file/sha1
         racket/random)

(define (random-bytes n)
  (list->bytes
   (for/list ([i (in-range n)])
    (random 256))))

(define-syntax-rule (with-random-seed seed body ...)
  (parameterize ([current-pseudo-random-generator (make-pseudo-random-generator)])
    (random-seed seed)
    body ...))

(define (test-random-bytes b)
  (let-values ([(status headers content)
                (http-sendrecv "rest.randomsanity.org"
                               (string-append "/v1/q/"
                                              (bytes->hex-string b)))])
    (let ([text (port->string content)])
      (cond
        [(string=? text "true") #t]
        [(string=? text "false") #f]
        [else (raise-result-error 'test-random-bytes
                                  "true or false" text)]))))

(test-random-bytes (make-bytes 64 0))        ; ==> #f
(with-random-seed 1234567890
  (test-random-bytes (random-bytes 64)))     ; ==> #f
(test-random-bytes (random-bytes 64))        ; ==> #t
(test-random-bytes (crypto-random-bytes 64)) ; ==> #t