EVM-Puzzles es una colección de desafíos que lo ayudarán a comprender mejor la máquina virtual Ethereum. Cada rompecabezas comienza brindándole una serie de códigos de operación y le pide que ingrese el valor de transacción correcto o los datos de llamada que permitirán que la secuencia se ejecute sin revertir. Este recorrido tiene como objetivo ser una guía de bajo impacto para cada rompecabezas, lo que facilita que cualquier persona con cualquier nivel de experiencia comprenda completamente el por qué y el cómo detrás de cada solución. Este tutorial asumirá que está familiarizado con las máquinas apiladoras. Si no es así, eche un vistazo a cómo funcionan las máquinas apiladoras antes de comenzar. Es útil saber que cada elemento en la pila en el EVM tiene 32 byes (es decir, una palabra). En este repositorio, hay 10 rompecabezas. Para alguien sin experiencia con EVM, esto debería tomar alrededor de 1 a 2 horas. Para alguien con experiencia básica en EVM, esto debería tomar alrededor de 1 hora. Si se siente muy cómodo con el EVM pero aún desea realizar el tutorial, esto debería tomar alrededor de 30 minutos. Con esa nota, ¡estamos listos para comenzar!
Primero, diríjase al repositorio de EVM-Puzzles , clone el proyecto y configure su entorno local. Asegúrese de tener instalado el casco. Si no lo hace, simplemente ingrese npm install --save-dev hardhat
cuando esté en la carpeta del proyecto raíz.
A continuación, si es nuevo en EVM, eche un vistazo breve a los códigos de operación de EVM (no sienta la necesidad de entender todo, solo tenga una idea general).
Con todo eso fuera del camino, echemos un vistazo al primer rompecabezas. Para iniciar el primer rompecabezas, ingrese al directorio raíz del proyecto e ingrese npx hardhat play
en la terminal.
Echemos un vistazo al primer rompecabezas. Se le da una serie de códigos de operación que representan un contrato. El acertijo le solicita que ingrese un valor para enviar, o en otras palabras, si envió una transacción a este contrato, ¿cuál debería ser el valor de la transacción para que este contrato se ejecute sin presionar la instrucción REVERT ? Adelante, pruébalo y luego siéntete libre de volver aquí si te quedas atascado o si quieres ver en profundidad la solución después de resolver el rompecabezas.
############ # Puzzle 1 # ############ 00 34 CALLVALUE 01 56 JUMP 02 FD REVERT 03 FD REVERT 04 FD REVERT 05 FD REVERT 06 FD REVERT 07 FD REVERT 08 5B JUMPDEST 09 00 STOP ? Enter the value to send: (0)
Bien, ahora para la explicación. Primero, necesitamos saber qué hace la instrucción CALLVALUE . Este código de operación obtiene el valor de la llamada actual (es decir, el valor de la transacción) en wei y empuja ese valor a la parte superior de la pila. Entonces, si ingresamos un valor de 10, antes de que la instrucción CALLVALUE
sea
evaluado, la pila se vería así.
[0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
Después de evaluar el código de operación CALLVALUE
, la pila se vería así.
[10 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
A continuación, necesitamos saber qué hace la instrucción JUMP . Este código de operación consume el valor superior en la pila y salta a la n
-ésima instrucción en la secuencia donde n
es el valor en la parte superior de la pila. Un ejemplo rápido hará esto más claro. Digamos que tenemos la siguiente secuencia.
00 34 CALLVALUE 01 56 JUMP 02 FD REVERT 03 FD REVERT 04 80 JUMPDEST 05 80 DUP1 06 00 STOP ? Enter the value to send: (0)
Si ingresamos 4 como el valor a enviar, el código de operación CALLVALUE
empujará 4
a la pila. Después de CALLVALUE
, ahora nuestra pila se ve así.
[4 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
Luego, el código de operación JUMP
consume el valor superior en la pila y salta a la instrucción en esa posición. Dado que el valor en la parte superior de la pila es 4
, el contador del programa salta a la cuarta instrucción y continúa. Un código de operación JUMP
debe alterar el contador del programa para terminar en una instrucción JUMPDEST
. Para el ejemplo anterior, podemos pensar en el programa con este aspecto después de evaluar la instrucción JUMP
.
05 80 DUP1 06 00 STOP
Ahora que todo eso está claro, volvamos al rompecabezas. Necesitamos ingresar un valor para que el programa se ejecute sin presionar una instrucción REVERT
.
00 34 CALLVALUE 01 56 JUMP 02 FD REVERT 03 FD REVERT 04 FD REVERT 05 FD REVERT 06 FD REVERT 07 FD REVERT 08 5B JUMPDEST 09 00 STOP
Para hacer esto, podemos ingresar un valor de llamada de 8, lo que hace que la instrucción CALLVALUE
empuje 8
a la pila donde la instrucción JUMP
luego consume ese valor y salta a la octava instrucción, omitiendo todas las instrucciones REVERT
. ¡Buen trabajo, un rompecabezas menos!
Ahora que tienes los pies mojados, echemos un vistazo al segundo rompecabezas. Pruébelo usted mismo y, al igual que antes, siéntase libre de volver para ver la solución y la explicación. Aquí está el rompecabezas.
############ # Puzzle 2 # ############ 00 34 CALLVALUE 01 38 CODESIZE 02 03 SUB 03 56 JUMP 04 FD REVERT 05 FD REVERT 06 5B JUMPDEST 07 00 STOP 08 FD REVERT 09 FD REVERT ? Enter the value to send: (0)
Al igual que antes, debemos ingresar un valor de transacción para enviar que hará que el programa se ejecute sin revertir. Si echamos un vistazo a la secuencia de instrucciones, podemos ver que necesitamos el código de operación JUMP
para alterar el contador del programa a la sexta instrucción. Al igual que antes, la primera instrucción es CALLVALUE
, por lo que sabemos que el valor que ingresamos terminará en la parte superior de la pila después de la primera instrucción.
Echemos un vistazo a la instrucción CODESIZE . Este código de operación obtiene el tamaño del código que se ejecuta en el entorno actual. En este ejemplo, podemos verificar manualmente el tamaño del código observando cuántos códigos de operación hay en la secuencia. Cada código de operación es de 1 byte, y en este rompecabezas tenemos 10 códigos de operación, lo que significa que el tamaño del código es de 10 bytes. Como nota al margen importante, el EVM usa números hexadecimales para representar el código de bytes. Si no está familiarizado, vea cómo funcionan los números hexadecimales . Con esto en mente, podemos saber que 0a
se coloca en la pila, lo que representa 10 bytes.
El siguiente código de operación con el que nos encontramos es la instrucción SUB , que toma el primer elemento de la pila menos el segundo elemento de la pila, colocando el resultado en la parte superior de la pila. Se consumen ambas entradas en la parte superior de la pila antes de la instrucción SUB
. Por ejemplo, si tuviéramos una pila que se viera así.
[3 2 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
Ejecutar la instrucción SUB
produciría el siguiente resultado de pila.
[1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
Con esta información volvamos al rompecabezas. Ahora sabemos que el programa primero evalúa la instrucción CALLVALUE
, colocando el valor que ingresamos en la pila. Luego, el programa evalúa la instrucción CODESIZE
, que inserta 0a
(que representa 10 bytes) en la pila. También sabemos que necesitamos JUMP
para cambiar el contador del programa a la sexta instrucción. Así es como se ve la pila después de la instrucción CODESIZE
.
[a your_input 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
Si aún no ha terminado el rompecabezas, continúe e intente usar la información anterior para ingresar el valor correcto. De lo contrario, siéntase libre de seguir leyendo para conocer el último paso de la solución.
Como sabemos que la instrucción SUB
es la siguiente, debemos ingresar un valor tal que 0a - your_input
igual a 6
, lo que hace que nuestra respuesta sea 4.
Prepárate para cambiar de marcha un poco. En lugar de ingresar un valor de transacción para resolver el rompecabezas, tendremos que ingresar los datos de la llamada. Calldata es un espacio direccionable por bytes de solo lectura donde se guardan los datos de transacción durante un mensaje o una llamada. En lenguaje sencillo, esta es la carga útil del código de bytes que se adjunta a un mensaje ( haga clic aquí para obtener más información sobre la anatomía de una transacción en Ethereum ). Echemos un vistazo al rompecabezas.
############ # Puzzle 3 # ############ 00 36 CALLDATASIZE 01 56 JUMP 02 FD REVERT 03 FD REVERT 04 5B JUMPDEST 05 00 STOP ? Enter the calldata:
Para este rompecabezas, es útil saber que 1 byte son 8 bits y que los números del 0 al 255 pueden representar un byte en el EVM. Este rompecabezas también nos presenta un nuevo código de operación llamado CALLDATASIZE . Esta instrucción obtiene el tamaño de los datos de la llamada en bytes y los coloca en la pila.
Con ese conocimiento, esto hace que el rompecabezas sea bastante sencillo. Tendremos que pasar datos de llamada de modo que la instrucción CALLDATASIZE
empuje 4 en la pila. A partir de ahí, la instrucción JUMP
saltará a la cuarta instrucción de la secuencia, llegando a JUMPDEST
. Para mantenerlo simple, 0xff
se puede usar para representar 1 byte ya que ff
en hexadecimal se evalúa como 255 en formato decimal. Todo lo que tenemos que hacer es copiar ff
cuatro veces, haciendo que el código de bytes que debemos ingresar sea: 0xffffffff
. ¡Otro rompecabezas caído!
Ingrese bit a bit. En este rompecabezas vemos nuestra primera instrucción XOR
. Como de costumbre, siéntete libre de intentarlo y descubrirlo por tu cuenta. Cuando esté listo, regrese aquí para encontrar la solución y la explicación.
############ # Puzzle 4 # ############ 00 34 CALLVALUE 01 38 CODESIZE 02 18 XOR 03 56 JUMP 04 FD REVERT 05 FD REVERT 06 FD REVERT 07 FD REVERT 08 FD REVERT 09 FD REVERT 0A 5B JUMPDEST 0B 00 STOP ? Enter the value to send: (0)
Sabemos que CALLVALUE
empujará el valor que ingresamos a la parte superior de la pila. También podemos saber qué tan grande es el CODESIZE
observando cuántas instrucciones hay. En este programa, tenemos 12 instrucciones, lo que hace 12 bytes o 0c
en hexadecimal, que se envía a la pila. Así que ahora nuestra pila se ve así.
[c your_input 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
Echemos un vistazo a la instrucción XOR . Esta instrucción evalúa dos números en su representación binaria y devuelve un 1
en cada posición de bit donde los bits de cualquiera de los operandos, pero no de ambos, son 1
s. Echemos un vistazo a un ejemplo rápido. Digamos que tenemos dos números en la parte superior de la pila.
[5 3 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
Al ejecutar la instrucción XOR
, podemos imaginar los dos números en representación binaria así.
00000000000000000000000000000101 00000000000000000000000000000011
Luego, poco a poco, los dos números se evalúan uno contra el otro. Si un bit es un 0
y el otro bit es un 1
, el bit resultante será un 1
, si ambos bits son 0
s o ambos bits son 1
s, el número resultante es un 0
. Así que el resultado de 5 XOR 3
es este.
00000000000000000000000000000110
De vuelta al rompecabezas. Sabemos que tenemos 0c
en la parte superior de la pila y your_input
en la segunda posición de la pila. Después del XOR
, el código de operación JUMP
debe enviarnos a la décima instrucción. Ahora, con toda esta información conocida, solo necesitamos ingresar un valor de llamada para que el 0c XOR callvalue
en 10
hexadecimal. Adelante, pruébalo por tu cuenta.
Bien, ahora para los pasos finales. Sabemos que necesitamos que el resultado de XOR
sea 10
, que en binario se representa como 1010
. También tenemos 0c
en la pila, que en binario se representa como 1100
. Así que ahora necesitamos encontrar un número tal que c XOR your_input
resulte en 1010
, lo que hace que el número que necesitamos ingresar sea 0110
. Esto se evalúa como el número hexadecimal 06
. ¡6 es nuestra respuesta!
Bienvenido al siguiente rompecabezas, donde nos encontramos con algunos códigos de operación nuevos. Siéntete libre de darle una oportunidad. Mientras tanto, echemos un vistazo a la secuencia de instrucciones de este rompecabezas.
############ # Puzzle 5 # ############ 00 34 CALLVALUE 01 80 DUP1 02 02 MUL 03 610100 PUSH2 0100 06 14 EQ 07 600C PUSH1 0C 09 57 JUMPI 0A FD REVERT 0B FD REVERT 0C 5B JUMPDEST 0D 00 STOP 0E FD REVERT 0F FD REVERT ? Enter the value to send: (0)
DUP1
se encuentra con el lector, el lector se encuentra con DUP1
. La instrucción DUP1 es bastante sencilla. Duplica el valor en la primera posición de la pila y empuja el duplicado a la parte superior de la pila. De manera similar, DUP2
duplicaría el valor en la segunda posición de la pila y empujaría el valor duplicado hacia la parte superior. Hay instrucciones DUP para todas las posiciones en la pila ( DUP1-DUP16
).
Echando un vistazo a las dos primeras instrucciones del rompecabezas, primero se ejecuta CALLVALUE
, empujando el valor que ingresamos a la parte superior de la pila. Luego se ejecuta DUP1
, duplicando el valor que ingresamos y empujándolo a la parte superior de la pila. Ahora, después de las dos primeras instrucciones, nuestra pila se ve así.
[your_input your_input 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
Entonces nos encontramos con otra nueva instrucción. La instrucción MUL toma los dos primeros valores de la pila, los multiplica y coloca el resultado en la parte superior de la pila. Entonces, en este caso, your_input
se multiplica por your_input
y la pila resultante se ve así.
[mul_result 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
A continuación nos encontramos con la instrucción PUSH2 . Esta instrucción empuja un valor de 2 bytes en la parte superior de la pila. Cuando vea cualquier instrucción PUSH
, siempre estará acompañada por el valor que empujará. Por ejemplo, en nuestro rompecabezas tenemos PUSH2 0100
, lo que significa que empujará el número hexadecimal de 2 bytes 0100
en la parte superior de la pila. Hay instrucciones push de PUSH1
a PUSH32
.
Volviendo a nuestro rompecabezas, dado que la siguiente instrucción es PUSH2 0100
, nuestra pila resultante ahora se verá así.
[0100 mul_result 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
Ahora nos encontramos con la instrucción EQ . Esta instrucción toma los dos primeros valores de la pila, ejecuta una comparación de igualdad y coloca el resultado en la parte superior de la pila. Si los dos primeros valores son iguales, 1
se coloca en la parte superior; de lo contrario, 0
se coloca en la pila. Ambos valores en las posiciones 1 y 2 de la pila se consumen de la instrucción EQ
.
Para simplificar, digamos que mul_result
es 0100
, de modo que cuando se evalúa la instrucción EQ
, se empuja 1
a la pila, haciendo que nuestra pila ahora se vea así.
[1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
La siguiente instrucción que se evalúa es PUSH1 0C
que empuja 0c
a la parte superior de la pila. Siguiendo esta instrucción, vemos otra nueva instrucción. La instrucción JUMPI alterará condicionalmente el contador del programa. Esta instrucción mira el segundo elemento de la pila para saber si debe saltar o no, dependiendo de si el segundo elemento de la pila es un 1
o un 0
. Luego, el primer elemento de la pila se usa para saber a qué posición saltar. La instrucción JUMPI
consume ambos valores en la parte superior de la pila durante este proceso. Así que echando un vistazo a nuestro rompecabezas, después de la instrucción PUSH1 0c
, nuestra pila se ve así.
[0c 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
Primero, la instrucción JUMPI
verifica el segundo elemento de la pila. En este caso es 1
indicando que el programa debe saltar. Luego, JUMPI
verifica el primer elemento de la pila para saber a dónde debe saltar. El valor de la pila superior es 0c
, lo que significa que saltará a la instrucción número 12, que es nuestro JUMPDEST
.
¡Y eso completará nuestro rompecabezas! Entonces, con toda esta información, ahora sabemos que debemos ingresar un valor de llamada para que cuando se duplique una vez (haciendo que los dos primeros elementos en la pila sean el valor de llamada), y después de que se multipliquen los valores de la pila superior, nuestro resultado sea el número hexadecimal 0100
. Siéntase libre de intentarlo desde aquí y ver si puede resolverlo.
Ok ahora para los pasos finales. Podemos convertir 0100
en un número decimal y obtener 256. Luego podemos sacar la raíz cuadrada de 256 ya que DUP1 MUL
esencialmente multiplica el número por sí mismo. ¡El número resultante es 16, que es la respuesta a este acertijo!
¡5 acertijos abajo, 5 para terminar! Como de costumbre, pruebe el rompecabezas y luego vuelva aquí para encontrar la solución y la explicación.
############ # Puzzle 6 # ############ 00 6000 PUSH1 00 02 35 CALLDATALOAD 03 56 JUMP 04 FD REVERT 05 FD REVERT 06 FD REVERT 07 FD REVERT 08 FD REVERT 09 FD REVERT 0A 5B JUMPDEST 0B 00 STOP ? Enter the calldata:
Saluda a la instrucción CALLDATALOAD . Esta instrucción obtiene los datos de entrada de los datos de llamada adjuntos a una transacción. Hay algunas cosas importantes a tener en cuenta sobre este código de operación. CALLDATALOAD
espera un número entero en la parte superior de la pila para saber desde qué byte empezar a cargar los datos de la llamada. Por ejemplo, si envía una transacción con una secuencia de 32 bytes como datos de llamada y coloca 08
en la parte superior de la pila, cuando ejecute CALLDATALOAD
, todos los datos de llamada del byte 8 al byte 32 se colocarán en la parte superior de la pila. Como nota adicional, si los datos de llamada son 64 bytes y necesita acceder a los segundos 32 byes de la secuencia, puede insertar 20
en la pila y luego usar CALLDATALOAD
para obtener los segundos 32 byes de la secuencia.
Ahora volvamos al rompecabezas. Podemos ver que hay PUSH1 00
seguido de CALLDATALOAD
, lo que significa que los datos de la llamada se cargarán a partir del byte 0 y los bytes 0-32 de los datos de la llamada se colocarán en la parte superior de la pila. Podemos ver que la instrucción JUMP
necesita alterar el contador del programa a 0a
(es decir, la décima instrucción). Siéntase libre de detenerse aquí e intentar resolver el resto del rompecabezas.
Bien, repasemos los pasos finales. Sabemos que los datos de llamada están en hexadecimal, por lo que puede parecer intuitivo ingresar 0x0a
como datos de llamada para completar el rompecabezas, pero es posible que haya notado que esto no funciona. Esto se debe a que cuando se envía calldata, dado que la secuencia de bytes no era de 32 bytes, se rellena a la derecha, por lo que lo que pensamos que era 0a
, en realidad se convierte en a00000000000000000000000000000000000000000000000000000000000000
. Entonces, lo que debemos hacer es rellenar nuestro 0x0a
con 31 bytes a la izquierda, convirtiéndolo en 0x000000000000000000000000000000000000000000000000000000000000000a
. Ahí lo tienes, ¡esa es nuestra respuesta!
Ya sabes que hacer. Pruebe el rompecabezas y luego regrese para ver la solución / explicación completa.
############ # Puzzle 7 # ############ 00 36 CALLDATASIZE 01 6000 PUSH1 00 03 80 DUP1 04 37 CALLDATACOPY 05 36 CALLDATASIZE 06 6000 PUSH1 00 08 6000 PUSH1 00 0A F0 CREATE 0B 3B EXTCODESIZE 0C 6001 PUSH1 01 0E 14 EQ 0F 6013 PUSH1 13 11 57 JUMPI 12 FD REVERT 13 5B JUMPDEST 14 00 STOP ? Enter the calldata:
Lo primero es lo primero, podemos ver CALLDATASIZE
y saber que necesitaremos ingresar calldata con un tamaño específico para resolver este rompecabezas. Tomemos nota de esto y volvamos más tarde. Después de que el tamaño de los datos de la llamada se inserta en la pila, hay PUSH1 00
y DUP1
, lo que hace que nuestra pila en este punto se vea así.
[0 0 calldata_size 0 0 0 0 0 0 0 0 0 0 0 0 0]
A continuación nos encontramos con la instrucción CALLDATACOPY . Esta instrucción copia los datos de entrada de la transacción y los guarda en la memoria. Este código de operación espera tres elementos en la parte superior de la pila que son [destOffset offset size]
, en este orden. destOffset
es el desplazamiento de bytes en la memoria donde se copiará el resultado. No hemos hablado mucho sobre la memoria en este momento y si desea obtener más información, puede leer al respecto aquí . La versión abreviada es que hay una estructura de datos temporal que asigna espacio para contener valores durante la ejecución de una función y destOffset
le dice al programa en qué ranura de la memoria almacenar los datos que se copian de calldata. El offset
dicta desde dónde comenzar a copiar los datos de la llamada (tal como lo hace CALLDATALOAD
en el último ejemplo) y el size
le dice al programa cuánto de la secuencia de bytes almacenar en la memoria. Durante este proceso, se consumen los tres elementos superiores de la pila.
Con todo esto conocido, revisemos nuestra pila actual.
[0 0 calldata_size 0 0 0 0 0 0 0 0 0 0 0 0 0]
Cuando se ejecuta la instrucción CALLDATALOAD
, almacenará los datos de llamada en la ranura de memoria 0
, comenzando en el byte 0
y almacenando el tamaño de todos los datos de llamada. Nuestra pila resultante después de esta instrucción se ve así.
[0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
Inmediatamente después, CALLDATASIZE
PUSH1 00
PUSH1 00
, haciendo que la pila se vea como la teníamos antes CALLDATALOAD
.
[0 0 calldata_size 0 0 0 0 0 0 0 0 0 0 0 0 0]
A continuación, se nos presenta otro código de operación nuevo, la instrucción CREAR . Esta instrucción crea una nueva cuenta (es decir, contrato o EOA). Analicemos un poco el capó con el código de operación CREATE
, ya que esto será útil más adelante durante el tutorial (y generalmente es bueno saberlo).
Al implementar un nuevo contrato con el código de operación CREATE
, la pila debe tener [value offset size]
en la parte superior de la pila, en este orden. El value
es la cantidad de wei para enviar el nuevo contrato que se está creando, el offset
es la ubicación en la memoria donde comienza el código de bytes que se ejecutará en la implementación y el size
es el tamaño del código de bytes que se ejecutará en la implementación. Cuando implementa un contrato con el código de operación CREATE
, el código de bytes del offset
no es el código de bytes del nuevo contrato, sino que el código de bytes del offset
se ejecuta durante la implementación y el valor devuelto se convierte en el código de bytes del contrato recién creado.
Veamos un ejemplo rápido que hará que esto sea fácil de entender. Si usa el código de operación CREATE
con el código de bytes de implementación de 0x6160016000526002601Ef3 , dado que el valor de retorno de esta secuencia de código de bytes es 6001
, el código de bytes del contrato recién creado será 6001
, es decir. PUSH1 01
. Entonces, cuando llame a este contrato, ¡simplemente ejecutará PUSH1 01
! Asegúrese de tomar nota de este concepto, ya que será útil más adelante.
Cuando se ejecuta la instrucción CREATE
, se consumen los tres valores y la dirección en la que se implementó la cuenta se coloca en la parte superior de la pila. Después de que se ejecuta este código de operación, nuestra pila se ve así.
[address_deployed 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
A continuación, nos encontramos con la instrucción EXTCODESIZE que espera una dirección en la parte superior de la pila y devuelve el tamaño del código en esa dirección. La dirección en la parte superior de la pila se consume en este proceso. Después EXTCODESIZE
vemos PUSH1 01
haciendo que nuestra pila se vea así.
[01 address_code_size 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
Inmediatamente después, se ejecuta la instrucción EQ
, comprobando si los dos valores superiores son iguales y colocando el resultado en la pila. A partir de ahí, PUSH1 13
y JUMPI
se utilizan para llevarnos al JUMPDEST
. Entonces, volviendo al comienzo del rompecabezas, ¡tendremos que ingresar los datos de llamada de manera que el tamaño del código sea igual a 01 byte! Esto es un poco complicado, así que para entenderlo, podemos ver el ejemplo del patio de recreo de la instrucción EXTCODESIZE . Así es como se ve el ejemplo.
// Creates a constructor that creates a contract with 32 FF as code PUSH32 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF PUSH1 0 MSTORE //Opcodes to return 32 ff PUSH32 0xFF60005260206000F30000000000000000000000000000000000000000000000 PUSH1 32 MSTORE // Create the contract with the constructor code above PUSH1 41 PUSH1 0 PUSH1 0 CREATE // Puts the new contract address on the stack // The address is on the stack, we can query the size EXTCODESIZE
Echemos un vistazo más de cerca a los códigos de operación en el constructor.
// Push a 32 byte value onto the stack PUSH32 FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF PUSH1 00 // Store the 32 byte value at memory slot 0 MSTORE // Return a 32 byte value starting at memory slot 0 PUSH1 20 PUSH1 00 RETURN STOP STOP ...
Cuando se ejecuta este código, devuelve un valor de ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
que es de 32 bytes. Si cambiamos el tamaño de retorno a 16 bytes en lugar de 32 bytes, EXTCODESIZE
será 10
, que es 16 bytes en hexadecimal. Esto indica que EXTCODESIZE
usa el tamaño del valor devuelto para dictar el tamaño del código.
Vamos a terminar el rompecabezas. Ahora sabemos que EXTCODESIZE
evalúa el tamaño del valor de retorno del código de bytes implementado. Con esta información, podemos pasar datos de llamada de manera que cuando se implemente, ¡devuelva un valor de 1 byte! Puede usar cualquier secuencia de códigos de operación que devuelva 1 byte, pero para este tutorial, usaremos 0x60016000526001601ff3
. Y con eso, ¡otro rompecabezas resuelto!
Bienvenido al octavo rompecabezas. Echemos un vistazo a lo que está en la tienda.
############ # Puzzle 8 # ############ 00 36 CALLDATASIZE 01 6000 PUSH1 00 03 80 DUP1 04 37 CALLDATACOPY 05 36 CALLDATASIZE 06 6000 PUSH1 00 08 6000 PUSH1 00 0A F0 CREATE 0B 6000 PUSH1 00 0D 80 DUP1 0E 80 DUP1 0F 80 DUP1 10 80 DUP1 11 94 SWAP5 12 5A GAS 13 F1 CALL 14 6000 PUSH1 00 16 14 EQ 17 601B PUSH1 1B 19 57 JUMPI 1A FD REVERT 1B 5B JUMPDEST 1C 00 STOP ? Enter the calldata:
Este puede parecer más desalentador, pero en realidad es bastante simple. Primero vemos un CALLDATASIZE PUSH1 00 DUP1 CALLDATACOPY CALLDATASIZE PUSH1 00 PUSH1 00 CREATE
muy similar que, al igual que el rompecabezas anterior, crea un nuevo contrato a partir de los datos de llamada que pasa y devuelve la dirección de implementación. Entonces, desde el principio, sabemos que tendremos que ingresar datos de llamada con código de bytes para un contrato para resolver el rompecabezas. Tomemos una nota mental rápida de cómo se ve la pila en este punto. Dado que la instrucción CREATE
consume los tres valores principales de la pila y envía la dirección en la que se implementó la cuenta, nuestra pila ahora se ve así.
[address_deployed 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
Las siguientes 5 instrucciones se relacionan con la instrucción CALL . Esta instrucción crea un nuevo subcontexto y ejecuta el código de la cuenta dada, luego reanuda la cuenta actual. En lenguaje sencillo, la instrucción CALL
se usa para interactuar con otro contrato. Este código de operación espera que la pila tenga algunos valores en la parte superior de la pila [gas address value argsOffset argsSize retOffset retSize]
, en este orden. Veamos cada uno de los argumentos uno por uno. gas
es la cantidad de gas que se enviará con el mensaje de llamada. address
es la dirección a la que se enviará el mensaje. value
es la cantidad de wei que se enviará con el mensaje. argsOffset
es la ubicación en la memoria dentro del contexto actual (es decir, el msg.sender) que se usará como datos de llamada para la llamada del mensaje. argsSize
es el tamaño de los datos de la llamada que se enviarán con la llamada del mensaje. retOffset
es la ubicación en la memoria dentro del contexto actual donde se almacenará el valor de retorno de la llamada. Finalmente, retSize
es el tamaño del valor de retorno que se almacenará en la memoria.
Ahora echemos un vistazo al rompecabezas de nuevo. Los siguientes cuatro códigos de operación son PUSH1 00 DUP1 DUP1 DUP1 DUP1
, lo que hace que la pila se vea así.
[0 0 0 0 0 address_deployed 0 0 0 0 0 0 0 0 0 0]
Luego vemos la instrucción SWAP5 . Esta instrucción intercambia los elementos de la pila 1 y 6. Hay instrucciones SWAP
para todas las posiciones en la pila ( SWAP1
- SWAP16
). En este caso, SWAP5
intercambia 0
con address_deployed
haciendo que nuestra pila ahora esté en el orden correcto para que coincida con [gas address value argsOffset argsSize retOffset retSize]
. Así es como se ve nuestra pila ahora.
[address_deployed 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
Luego ejecutamos la instrucción CALL
, que devuelve 0
si el subcontexto se revirtió y 1
si fue un éxito. Después de la instrucción CALL
, podemos ver un PUSH1 00 EQ
, lo que significa que necesitamos CALL
para insertar un 0
en la pila. Continúe y pruebe el resto del rompecabezas, luego siéntase libre de volver para ver el resto de la solución.
Bien, ahora sabemos que la instrucción CALL
debe devolver 0
, lo que significa que debemos ingresar los datos de llamada que hacen que CALL
falle. Para que CALL
falle, hay tres formas. Una forma en que puede fallar es si no hay suficiente gasolina. La segunda forma en que puede fallar es si no hay suficientes valores en la pila. La tercera forma en que puede fallar es si el contexto de ejecución actual es de una STATICCALL y el valor en wei (índice de pila 2) no es 0 (desde la bifurcación de Byzantium). También es importante tener en cuenta que CALL
siempre tendrá éxito cuando CALL
a una cuenta sin código (o tamaño de código de 0).
Para terminar este rompecabezas, volvamos a ver cómo funciona el código de operación CREATE
. Sabemos que el valor de retorno del código de bytes que se ejecuta en la implementación se convierte en el código de bytes del contrato recién creado. Con esa información conocida, podemos pasar datos de llamada con una secuencia de código de bytes de modo que el valor de retorno de la secuencia provoque un REVERT
cuando se ejecute.
Puede pasar cualquiera que resulte en REVERT
pero para el tutorial usaremos 0x60016000526001601ff3 como código de bytes de implementación. Dado que el valor de retorno de esta secuencia de código de bytes es 01
, el código del contrato recién creado será 01
, es decir. la instrucción ADD
. Entonces, cuando llame a este contrato, ejecutará la instrucción ADD
, y dado que no hay valores en la pila en el subcontexto del contrato, ¡la CALL
fallará (es decir, REVERT
)! Ahí lo tienes, 0x60016000526001601ff3
es nuestra respuesta.
Estamos en la recta final, echemos un vistazo al rompecabezas #9. Este rompecabezas agrega una capa más de complejidad, lo que requiere que ingrese un valor de llamada y datos de llamada para resolver el rompecabezas.
############ # Puzzle 9 # ############ 00 36 CALLDATASIZE 01 6003 PUSH1 03 03 10 LT 04 6009 PUSH1 09 06 57 JUMPI 07 FD REVERT 08 FD REVERT 09 5B JUMPDEST 0A 34 CALLVALUE 0B 36 CALLDATASIZE 0C 02 MUL 0D 6008 PUSH1 08 0F 14 EQ 10 6014 PUSH1 14 12 57 JUMPI 13 FD REVERT 14 5B JUMPDEST 15 00 STOP ? Enter the value to send: (0)
Ya estamos familiarizados con los dos primeros códigos de operación, por lo que podemos saber que después de las instrucciones CALLDATASIZE PUSH1 03
, nuestra pila se ve así.
[03 calldata_size 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
La instrucción LT ejecuta una comparación de los dos primeros valores de la pila para ver si el primer elemento de la pila es menor que el segundo elemento de la pila. Si LT
se evalúa como verdadero, se inserta 1
en la pila; de lo contrario, se inserta 0
en su lugar. Los dos valores utilizados en la comparación se consumen en este proceso. Por el bien del ejemplo, digamos que CALLDATASIZE
es de 4 bytes, por lo que LT
empujará 1
como resultado.
Dado que LT
se evaluó como verdadero, el código salta a JUMPDEST
en la instrucción 09
. Después del salto, CALLVALUE
y CALLDATASIZE
se colocan en la pila y MUL
los multiplica, consumiendo los dos valores superiores de la pila en el proceso. PUSH1 08
empuja 08
a la pila y luego EQ
verifica si el resultado de MUL
es igual a 08
, consumiendo los valores en el proceso. EQ
necesita empujar un 1
a la pila para permitir que JUMPI
nos lleve al final del rompecabezas.
Con toda esta información, ahora sabemos que necesitamos ingresar datos de llamada de modo que CALLDATASIZE
sea mayor a 3 bytes, y el producto de CALLDATASIZE * CALLVALUE
sea 08
.
Con un poco de matemática rápida, podemos usar cualquier combinación de valores que evalúen a 8 cuando se multiplican juntos que satisfagan las condiciones anteriores. Para el tutorial, ingresaremos 0x00000001
como datos de llamada y 2
como valor de llamada. ¡Un rompecabezas más para ir!
Aquí está, el rompecabezas final. Saltemos.
############# # Puzzle 10 # ############# 00 38 CODESIZE 01 34 CALLVALUE 02 90 SWAP1 03 11 GT 04 6008 PUSH1 08 06 57 JUMPI 07 FD REVERT 08 5B JUMPDEST 09 36 CALLDATASIZE 0A 610003 PUSH2 0003 0D 90 SWAP1 0E 06 MOD 0F 15 ISZERO 10 34 CALLVALUE 11 600A PUSH1 0A 13 01 ADD 14 57 JUMPI 15 FD REVERT 16 FD REVERT 17 FD REVERT 18 FD REVERT 19 5B JUMPDEST 1A 00 STOP ? Enter the value to send: (0)
En este rompecabezas, deberá ingresar un valor de llamada y datos de llamada. Echemos un vistazo a las primeras instrucciones. Primero vemos CODESIZE CALLVALUE SWAP1
que empuja el tamaño del código, seguido del valor de llamada que pasó y luego intercambia sus posiciones. En este punto, nuestra pila se ve así.
[1b callvalue 0 0 0 0 0 0 0 0 0 0 0 0 0]
A continuación vemos la instrucción GT que opera exactamente como LT
, pero evalúa mayor que en lugar de menor que. Para este acertijo, necesitamos que GT
empuje 1
en la pila, por lo que sabemos que nuestro valor de llamada debe ser menor que 1b
(es decir, 27 en notación decimal). Esto permitirá que el programa salte al primer JUMPDEST
en la instrucción 08
.
Ahora vemos CALLDATASIZE PUSH2 0003 SWAP1
que empuja el tamaño de los datos de llamada y 0003
a la pila e intercambia sus posiciones. Ahora nuestra pila se ve así.
[calldata_size 3 0 0 0 0 0 0 0 0 0 0 0 0 0]
A continuación vemos la instrucción MOD . Esta instrucción ejecuta un módulo del primer elemento de la pila y el segundo elemento de la pila, empujando el resto a la pila. Siguiendo la instrucción MOD
vemos la instrucción ISZERO , que empuja 1
a la pila si el valor superior de la pila es 0
. Si cualquier otro número está en la parte superior de la pila, 0
se empuja a la pila en su lugar. En nuestro caso, necesitamos ISZERO
para empujar 1
a la pila (volveremos a esto). Luego vemos CALLVALUE PUSH1 0A ADD
. La instrucción ADD simplemente suma los primeros dos valores en la pila y empuja el resultado a la pila. Siguiendo esta secuencia, hay un JUMPI
, lo que significa que CALLVALUE PUSH1 0A ADD
necesita empujar la posición de JUMPDEST
a la pila. Siéntase libre de darle una oportunidad al resto del rompecabezas desde aquí.
Con toda esta información, ahora sabemos algunas cosas. Primero, debemos ingresar los datos de llamada de modo que el tamaño de los datos de llamada sea divisible por 3 bytes, lo que permite que la CALLDATASIZE PUSH2 0003 SWAP1 MOD
0
en la pila. Esto le permite a ISZERO
empujar un 1
a la pila, donde el programa puede saltar al segundo JUMPDEST
. En segundo lugar, debemos ingresar un valor de llamada de modo que el valor sea menor que 26 y el valor de callvalue + 0a
sea igual a 0x19
. Con estos factores conocidos, podemos ingresar 0x000001
como datos de llamada y 15
(en decimal) como valor de llamada. ¡Así de simple, hemos completado el rompecabezas final!