Comprender el código genético de los criptocóticos

... y aprenda a trabajar con las herramientas de desarrollo de Ethereum utilizando un ejemplo de la vida real.

Parte cero: el objeto apareció a la vista


Acabo de terminar mis conferencias sobre el curso completo de desarrollo de aplicaciones descentralizadas basadas en Ethereum en Solidity en chino. Lo di en mi tiempo libre para aumentar el nivel de conocimiento sobre blockchain y contratos inteligentes entre la comunidad china de desarrolladores. Durante mi trabajo me hice amigo de un par de estudiantes.

Y justo al final del curso, de repente nos encontramos rodeados de estas criaturas:



Imagen de cryptokitties.co

Como la mayoría de las personas que se han encontrado con este fenómeno, nosotros tampoco pudimos resistir estas criptocreaciones lindas y rápidamente nos volvimos adictos al juego. Nos gustó sacar nuevos gatos e incluso reemplazamos el método del patito con el método del gato criptográfico . Creo que la adicción a los juegos es mala, pero no en este caso, porque la pasión por la cría de gatitos nos llevó rápidamente a la pregunta:

¿Cómo obtienen ciertos criptogatos su conjunto de genes?


Decidimos dedicar el sábado por la noche a encontrar la respuesta, y creemos que logramos progresar en el desarrollo de software que nos permite determinar la mutación genética de los cripto gatitos recién nacidos antes de que nazcan. En otras palabras, este programa puede ayudarlo a verificar y determinar el momento apropiado para la fecundación de la gata madre y, por lo tanto, obtener la más interesante de las posibles mutaciones.

Publicamos este material con la esperanza de que sirva a todos como un artículo introductorio para familiarizarse con herramientas de desarrollo de Ethereum muy útiles, al igual que los propios cripto gatitos permitieron que muchas personas no familiarizadas con blockchain se unan a las filas de los usuarios de criptomonedas.

Primera parte: la lógica de alto nivel de generar gatitos


Para empezar, nos preguntamos: ¿cómo es el nacimiento de los cripto gatitos?

Para responder a esto, utilizamos el excelente conductor de cadena de bloques Etherscan, que nos permite hacer mucho más que simplemente "estudiar los parámetros y el contenido de los bloques". Entonces descubrimos el código fuente del contrato CryptoKittiesCore:



https://etherscan.io/address/0x06012c8cf97bead5deae237070f9587f8e7a266d#code

Tenga en cuenta que el contrato ampliado es en realidad un poco diferente del utilizado en el programa de recompensas. Según este código, un gatito bebé se forma en dos pasos: 1) la gata es fertilizada por la gata; 2) un poco más tarde, cuando termina el período de maduración de la fruta, se llama a la función giveBirth. Por lo general, esta función es invocada por cierto demonio de proceso, pero, como verá más adelante, para obtener mutaciones interesantes, deberá seleccionar correctamente el bloque en el que nació su gatito.

function giveBirth(uint256 _matronId) external whenNotPaused returns(uint256) { Kitty storage matron = kitties[_matronId]; // ,  - . require(matron.birthTime != 0); // ,       ! require(_isReadyToGiveBirth(matron)); //      -. uint256 sireId = matron.siringWithId; Kitty storage sire = kitties[sireId]; // ,         uint16 parentGen = matron.generation; if (sire.generation > matron.generation) { parentGen = sire.generation; } //     . uint256 childGenes = geneScience.mixGenes(matron.genes, sire.genes, matron.cooldownEndBlock - 1); 

En el código anterior, puede ver claramente que los genes de un gatito recién nacido se determinan justo en el momento del nacimiento llamando a la función mixGenes del contrato inteligente externo geneScience. Esta función toma tres parámetros: el gen madre, el gen padre y el número de bloque en el que el gato estará listo para dar a luz.

Probablemente tendrá una pregunta lógica, ¿por qué los genes no están determinados en el momento de la concepción, como es el caso en el mundo real? Como verá en el curso de la narración posterior, esto le permite defenderse con bastante elegancia de los intentos de predecir y descifrar genes. Este enfoque elimina la posibilidad de una predicción 100% precisa de los genes del gatito antes de que se registre el hecho del embarazo gato-madre en la cadena de bloques. E incluso si pudiera encontrar el código exacto responsable de mezclar los genes, esto no le daría ninguna ventaja.

Sea como fuere, al principio aún no sabíamos esto, así que continuemos. Ahora necesitamos averiguar la dirección del contrato de geneScience. Para hacer esto, use MyEtherWallet:



Dirección del contrato de GeneScience

Así es como se ve el código de bytes del contrato:

 0x60606040526004361061006c5763ffffffff7c01000000000000000000000000000000000000000000000000000000006000350416630d9f5aed81146100715780631597ee441461009f57806354c15b82146100ee57806361a769001461011557806377a74a201461017e575b600080fd5b341561007c57600080fd5b61008d6004356024356044356101cd565b604051908152602001604051809........ 

Por su apariencia, no puede decir que, como resultado, algo tan lindo como un gatito aparece en todo, pero tenemos mucha suerte de que esta sea una dirección pública, y no necesitamos buscarla en el repositorio). De hecho, creemos que no debe hacerse tan fácilmente accesible. Si los desarrolladores realmente querían asegurarse de que la dirección del contrato fuera correcta, deberían usar la función checkScienceAddress, pero no nos molestará.

Segunda parte: el colapso de una hipótesis simple


Entonces, ¿qué queremos lograr al final? Debe entenderse que no nos fijamos el objetivo de compilar completamente el código de bytes, convirtiéndolo en un código de solidez legible por humanos. Necesitamos un método barato (sin la necesidad de pagar las transacciones en el blockchain de combate) para determinar los genes del gatito, siempre que sepamos quiénes son sus padres. Esto es lo que haremos.

Para comenzar, usemos la herramienta de código de operación Etherscan para un análisis rápido. Se ve así:



Mucho más claro

Seguimos la regla de oro de decodificar el código del ensamblador: comenzamos con una hipótesis simple y audaz sobre el comportamiento del programa y, en lugar de tratar de entender su trabajo en su conjunto, nos enfocamos en confirmar la suposición hecha. Revisaremos el código de bytes para responder algunas preguntas:

  1. ¿Utiliza marcas de tiempo? No, porque falta el código de operación TIMESTAMP. Si hay algún accidente simple en él, entonces su fuente es definitivamente otro código de operación.
  2. ¿Se utiliza un hash de bloque? Sí, BLOCKHASH ocurre dos veces. Por lo tanto, la aleatoriedad, si la hay, puede surgir de sus códigos de operación, pero aún no estamos seguros de esto.
  3. ¿Se utiliza algún hash? Sí, hay SHA3. Sin embargo, no está claro qué está haciendo.
  4. ¿Se utiliza msg.sender? No, porque falta el código de operación CALLER. Por lo tanto, no se aplica ningún control de acceso al contrato.
  5. ¿Se está utilizando algún contrato externo? No, no hay código de llamada CALL.
  6. ¿Se utiliza COINBASE? No, y por lo tanto excluimos otra posible fuente de aleatoriedad.

Después de recibir la respuesta a estas preguntas, presentamos y tenemos la intención de probar una hipótesis simple: el resultado de mixGene está determinado por tres y solo tres parámetros de entrada de esta función. Si esto es así, entonces podríamos simplemente implementar este contrato localmente, continuar llamando a esta función con los parámetros que nos interesan, y luego, tal vez, podríamos obtener un kit de genes de gatito incluso antes de la fertilización de la gata madre.

Para verificar esta suposición, llamamos a la función mixGene en la red principal con tres parámetros aleatorios: 1111115, 80, 40 y obtenemos algún resultado X. Luego, implemente este bytecode usando truffle y testrpc . Entonces, nuestra pereza condujo a una forma algo no estándar de usar trufa.

 contract GeneScienceSkeleton { function mixGenes(uint256 genes1, uint256 genes2, uint256 targetBlock) public returns (uint256) {} } 

Comenzamos con el esqueleto del contrato, lo colocamos en la estructura de carpetas del marco de trufas y ejecutamos la compilación de trufas. Sin embargo, en lugar de migrar directamente este contrato vacío a testrpc, reemplazamos el bytecode del contrato en la carpeta de compilación con el bytecode real expandido y el bytecode del contrato geneScience. Esta es una forma atípica pero rápida si desea implementar un contrato con solo bytecode y alguna interfaz abierta limitada para pruebas locales. Después de eso, llamamos directamente a Mixgenes con los parámetros 1111115, 80, 40, y desafortunadamente obtenemos un error con la respuesta revertir en respuesta. Ok, mira más profundo. Como sabemos, la firma de las funciones mixGene es 0x0d9f5aed, por lo que tomamos un bolígrafo y papel y rastreamos la ejecución del código de bytes, comenzando desde el punto de entrada de esta función para dar cuenta de los cambios en la pila y el almacenamiento. Después de algunos saltos, nos encontramos aquí:

 [497] DUP1 [498] NUMBER [499] DUP14 [500] SWAP1 [501] GT [504] PUSH2 0x01fe [505] JUMPI [507] PUSH1 0x00 [508] DUP1 [509] 'fd'(Unknown Opcode) 

A juzgar por el contenido de estas líneas, si el número del bloque actual es menor que el tercer parámetro, se llama a revert (). Bueno, este es un comportamiento bastante razonable: llamar a una función real en un juego con un número de bloque del futuro es imposible y esto es lógico.

Esta verificación de entrada es fácil de eludir: solo extraemos algunos bloques en testrpc y llamamos a la función nuevamente. Esta vez, la función devuelve con éxito Y.

Pero desafortunadamente X! = Y

Que mal. Esto significa que el resultado de la ejecución de la función depende no solo de los parámetros de entrada, sino también del estado de la cadena de bloques de la red principal, que, por supuesto, difiere del estado de la prueba falsa de blockchain.

Tercera parte: remangar nuestras mangas y excavar en la pila


Esta bien Así que es hora de arremangarse. El papel ya no es adecuado para rastrear el estado de la pila. Entonces, para un trabajo más serio, lanzaremos un desensamblador EVM muy útil llamado evmdis .

En comparación con el papel y la pluma, este es un paso tangible hacia adelante. Continuemos con lo que nos detuvimos en el último capítulo. La siguiente es una conclusión alentadora con evmdis:

 ............. :label22 # Stack: [@0x70E @0x70E @0x70E 0x0 0x0 0x0 @0x88 @0x85 @0x82 :label3 @0x34] 0x1EB PUSH(0x0) 0x1ED DUP1 0x1EE DUP1 0x1EF DUP1 0x1F0 DUP1 0x1F1 DUP1 0x1F3 DUP13 0x1F9 JUMPI(:label23, NUMBER() > POP()) # Stack: [0x0 0x0 0x0 0x0 0x0 0x0 @0x70E @0x70E @0x70E 0x0 0x0 0x0 @0x88 @0x85 @0x82 :label3 @0x34] 0x1FA PUSH(0x0) 0x1FC DUP1 0x1FD REVERT() :label23 # Stack: [0x0 0x0 0x0 0x0 0x0 0x0 @0x70E @0x70E @0x70E 0x0 0x0 0x0 @0x88 @0x85 @0x82 :label3 @0x34] 0x1FF DUP13 0x200 PUSH(BLOCKHASH(POP())) 0x201 SWAP11 0x202 POP() 0x203 DUP11 0x209 JUMPI(:label25, !!POP()) # Stack: [0x0 0x0 0x0 0x0 0x0 0x0 @0x70E @0x70E @0x70E 0x0 @0x200 0x0 @0x88 @0x85 @0x82 :label3 @0x34] 0x20C DUP13 0x213 PUSH((NUMBER() & ~0xFF) + (POP() & 0xFF)) 0x214 SWAP13 0x215 POP() 0x217 DUP13 0x21E JUMPI(:label24, !!(POP() < NUMBER())) # Stack: [0x0 0x0 0x0 0x0 0x0 0x0 @0x70E @0x70E @0x70E 0x0 @0x200 0x0 @0x213 @0x85 @0x82 :label3 @0x34] 0x222 DUP13 0x223 PUSH(POP() - 0x100) 0x224 SWAP13 0x225 POP() :label24 # Stack: [0x0 0x0 0x0 0x0 0x0 0x0 @0x70E @0x70E @0x70E 0x0 @0x200 0x0 [@0x223 | @0x213] @0x85 @0x82 :label3 @0x34] 0x227 DUP13 0x228 PUSH(BLOCKHASH(POP())) 0x229 SWAP11 0x22A POP() :label25 # Stack: [0x0 0x0 0x0 0x0 0x0 0x0 @0x70E @0x70E @0x70E 0x0 [@0x200 | @0x228] 0x0 [@0x88 | @0x223 | @0x213] @0x85 @0x82 :label3 @0x34] 0x22C DUP11 0x22D DUP16 0x22E DUP16 ........... 

Para lo que evmdis es realmente bueno es su utilidad para analizar JUMPDEST en las etiquetas correctas, que no se pueden sobreestimar.

Entonces, después de pasar el requisito inicial, nos encontramos en la etiqueta 23. Vemos DUP13 y recordamos del capítulo anterior que el número 13 en la pila es nuestro tercer parámetro. Así que estamos tratando de obtener el BLOCKHASH de nuestro tercer parámetro. Sin embargo, la acción de BLOCKHASH está limitada a 256 bloques. Es por eso que es seguido por JUMPI (esta es una construcción if). Si traducimos la lógica de los códigos de operación al lenguaje del pseudocódigo, obtenemos algo como esto:

 func blockhash(p) { if (currentBlockNumber - p < 256) return hash(p); return 0; } var bhash = blockhash(thrid); if (bhash == 0) { thirdProjection = (currentBlockNumber & ~0xff) + (thridParam & 0xff); if (thirdProjection > currentBlockNumber) { thirdProjection -= 256; } thirdParam = thirdProjection; bhash = blockhash(thirdProjection); } label 25 and beyond ..... some more stuff related to thirdParam and bhash 

algunas cosas más relacionadas con thirdParam y bhash - otro código relacionado con thirdParam y bloque hash

Ahora creemos que hemos encontrado una razón por la cual nuestros resultados difieren de los que observamos en la red principal. Más importante aún, aparentemente logramos descubrir la fuente del azar. A saber: el hash de bloque se calcula en función del tercer parámetro, o el pronóstico del tercer parámetro. Es importante tener en cuenta que en la pila el tercer parámetro también se reemplaza con este número de bloque predicho.

Obviamente, durante la ejecución local fuera de la red principal, no tenemos una opción simple para imponer un retorno BLOCKHASH que coincida con los valores de la red principal. Sea como fuere, dado que conocemos los tres parámetros, podemos monitorear fácilmente la red principal y obtener el hash del bloque H para el tercer parámetro, así como el hash del bloque predicho.

A continuación, podemos insertar este hash directamente en el código de byte en nuestro entorno de prueba local, y si todo va de acuerdo con el plan, finalmente obtendremos el conjunto correcto de genes.

Pero hay un inconveniente: DUP13 y BLOCKHASH son solo 2 bytes en el código, y si solo los reemplazamos con 33 bytes PUSH32 0x * hash *, el contador del programa cambiará completamente y tendremos que arreglar cada JUMP y JUMPI. O tendremos que hacer JUMP al final del código y reemplazar las instrucciones del código implementado, etc.

Bueno, como hemos llegado hasta aquí, olfatearemos un poco más. Dado que empujamos el hash de 32 bytes que no es cero en la rama if, la condición siempre será verdadera y, por lo tanto, todo lo escrito en la parte else puede simplemente descartarse para dejar espacio para nuestro hash de 32 bytes. Bueno, en general, esto es lo que hicimos:



El punto clave es que, dado que abandonamos la otra parte de la condición, debemos reemplazar el tercer parámetro de entrada de la función mixGene con el pronóstico del tercer parámetro antes de llamarlo.

Esto es hasta el punto de que si está tratando de obtener el resultado de una operación
mixGene (X, Y, Z), donde currentBlockNumber es Z <256, solo necesita reemplazar el hash PUSH32 con el hash del bloque Z.
Sin embargo, si tiene la intención de hacer lo siguiente
mixGene (X, Y, Z), donde currentBlockNumber es Z ≥ 256, deberá reemplazar el hash PUSH32 con el hash del bloque proj_Z, donde proj_Z se define de la siguiente manera:

 proj_Z = (currentBlockNumber & ~0xff) + (Z & 0xff); if (proj_Z > currentBlockNumber) { proj_Z -= 256; } <b>    Z  proj_Z   ,   mixGene(X, Y, proj_Z).</b> 

Tenga en cuenta que proj_Z permanecerá sin cambios en un cierto rango de bloques. Por ejemplo, si Z & 0xff = 128, entonces proj_Z cambia solo en cada bloque cero y 128o.

Para confirmar esta hipótesis y verificar si hay algún inconveniente por delante, cambiamos el código de bytes y usamos otra utilidad genial llamada hevm .



Si nunca ha usado hevm, le recomiendo que lo pruebe. La herramienta está disponible junto con su propio marco, pero sobre todo en su conjunto, debe tenerse en cuenta algo tan indispensable como un depurador de pila interactivo.

 Usage: hevm exec --code TEXT [--calldata TEXT] [--address ADDR] [--caller ADDR] [--origin ADDR] [--coinbase ADDR] [--value W256] [--gas W256] [--number W256] [--timestamp W256] [--gaslimit W256] [--gasprice W256] [--difficulty W256] [--debug] [--state STRING] Available options: -h,--help 

Arriba están las opciones de lanzamiento. La utilidad le permite especificar una variedad de parámetros. Entre ellos se encuentra --debug, que le brinda la capacidad de depurar de manera interactiva.

Entonces, aquí hemos realizado varias llamadas al contrato geneScience implementado en la cadena de bloques de la red principal y registrado los resultados. Luego usamos hevm para ejecutar nuestro bytecode roto con datos preparados teniendo en cuenta las reglas descritas anteriormente y ...

¡Los resultados son los mismos!

El último capítulo: conclusión y continuación del trabajo (?)


Entonces, ¿qué hemos podido lograr?

Usando nuestro software de pirateo, puede 100% de posibilidades de predecir un gen de 256 bits para un gatito recién nacido si nace en el rango de bloques [coolDownEndBlock (cuando el bebé está listo para aparecer), el bloque actual es + 256 (aproximadamente)]. Puede razonar sobre esto de esta manera: cuando el bebé está en el útero de la gata, sus genes mutan con el tiempo, debido a la fuente de entropía en forma de un hash del bloque coolDownEndBlock predicho, que también cambia con el tiempo. Por lo tanto, puede usar este programa para verificar cómo se verá el gen del bebé si nació en este momento. Y si no le gusta este gen, puede esperar unos 256 bloques más (en promedio) y verificar el nuevo gen.

Alguien puede decir que esto no es suficiente, ya que solo el 100% de precisión de la predicción puede considerarse un pirateo ideal incluso antes del embarazo de una madre gata. Sin embargo, esto no es posible, ya que el gen del gatito está determinado no solo por los genes de sus padres, sino también por el hash predicho del bloqueo como factor de mutación, que simplemente no se puede conocer antes de la fertilización.

¿Qué se puede mejorar y cuáles son los matices aquí?

Revisamos rápidamente los cambios que ocurren en la pila en la parte lógica real del contrato inteligente (etiqueta 25 y todo lo que sigue) y creemos que esta parte predecible del código mixGene está bastante sujeta a análisis y estudio. Esperamos que el hash de bloque como factor de mutación también tenga cierta importancia física, ayudando, por ejemplo, a determinar qué gen debe mutar. Si logramos resolver esto, obtendremos el gen original, sin mutaciones. Esto es útil porque si no tiene un buen gen fuente, incluso la mejor mutación puede no ser suficiente.

Tampoco medimos la correlación entre el gen de 256 bits y los rasgos del gatito (color de ojos, tipo de cola, etc.), pero creemos que esto es bastante posible con la ayuda de un bot de alto rendimiento y un clasificador simple.

Y, en general, comprendemos completamente la intención del equipo de desarrollo de CryptoKitties de estabilizar la mutación en un corto período de tiempo. Pero la otra cara de este enfoque es la capacidad de realizar un análisis como lo hicimos nosotros.

También nos gustaría agradecer a la maravillosa comunidad ethereum por desarrollar herramientas como Etherscan, hevm, evmdis, truffle, testrpc, myetherwallet y Solidity. Esta es una comunidad genial y estamos felices de ser parte de ella.

Y finalmente, el código modificado https://github.com/modong/GeneScienceCracked/

Recuerde cambiar $ CONSTBLOCKHASH $ al hash del bloque predicho.

imagen

Source: https://habr.com/ru/post/es409345/


All Articles