TimeLord construye semillas de Python que producen 1.000 caras seguidas
El proyecto demuestra que basta con elegir la semilla después de fijar el resultado deseado para que random.Random de CPython escupa cien caras seguidas, sin tocar el generador por dentro.
TimeLord es una demostración en Python que construye una semilla entera corriente para random.Random de CPython capaz de producir 100 caras consecutivas al lanzar una moneda con randrange(2). El rango llega hasta las 1.000. No hay un generador modificado, ni setstate(), ni parcheo en caliente, ni intervención alguna después de entregar la semilla: solo un número entero, eso sí, muy grande.
Cómo se fuerza el resultado
El módulo random de CPython usa MT19937, el Mersenne Twister. Para el lanzamiento de moneda, r.randrange(2) acaba llamando a getrandbits(2) y repite si el valor obtenido no es menor que 2. Esa función toma los dos bits altos de una palabra de 32 bits ya temperada. Para forzar el 1, que aquí es cara, basta con exigir que esos dos bits sean 01: como 01 ya es menor que 2, no hay rechazo y el sorteo sale a la primera.
Cada cara impone, por tanto, dos restricciones de bits. Cien caras son 200 restricciones; mil caras, 2.000. El estado de MT19937 ronda los 20.000 bits, así que queda muchísimo margen. La clave es que las transformaciones de twist y temper son lineales sobre GF(2): a nivel de bit se pueden escribir como un sistema de ecuaciones con XOR. TimeLord representa simbólicamente los bits del estado, los propaga por esas operaciones, monta las ecuaciones y las resuelve. Los bits que quedan libres se eligen a voluntad. El programa trabaja además varios bloques de salida, y por eso alcanza secuencias más largas que las 624 palabras del array de estado.
De estado interno a semilla
Lo interesante es lo que no hace. No usa setstate(), porque eso rebajaría la demostración: cualquiera diría que ahí hay trampa. En su lugar recorre al revés el procedimiento de inicialización por enteros de CPython, init_by_array. Halla las 624 palabras de 32 bits en little-endian que generan el estado recién construido y las combina en un único entero positivo, que es lo que se pasa a Random(seed). A partir de ahí, todo es Python estándar.
Los scripts no necesitan dependencias externas. find_heads_seed.py sin argumentos genera el caso de 100 caras y acepta hasta 1000; la semilla acaba en un fichero seed_<N>_heads.txt. Luego, demo_heads.py se limita a cargar el entero y usar la generación aleatoria normal: no contiene nada de la maquinaria de inversión, y esa separación es deliberada, para que se vea que durante el supuesto lanzamiento de moneda no ocurre nada raro. Hay también un modo texto que hace que el programa "aleatorio" escriba un mensaje fijado de antemano.
La probabilidad de sacar 100 caras es 2^-100, unos 7,9 × 10^-31, y la de sacar 1.000 es 2^-1000, unos 9,3 × 10^-302. Pero esa cifra solo es escalofriante si la semilla se eligió de antemano. Aquí se elige después, y cualquiera que reciba el entero reproduce la secuencia exacta en Python de serie. El ejemplo entra en la misma familia que la post-selección y el efecto look-elsewhere: calcular la probabilidad como si las condiciones previas se hubieran fijado solas es hacer trampa. Conviene recordarlo, sobre todo cuando alguien presume de un resultado imposible. Aquí no se rompe nada, porque MT19937 nunca se vendió como generador criptográfico; lo que se rompe es la intuición.