Comprender las especificaciones de la EVM del Libro Amarillo
El Libro Amarillo (opens in a new tab) es la especificación formal de Ethereum. Excepto donde ha sido modificado por el proceso de EIP, contiene la descripción exacta de cómo funciona todo. Está escrito como un artículo matemático, lo que incluye terminología con la que los programadores pueden no estar familiarizados. En este artículo aprenderá a leerlo y, por extensión, otros artículos matemáticos relacionados.
¿Qué Libro Amarillo?
Como casi todo lo demás en Ethereum, el Libro Amarillo evoluciona con el tiempo. Para poder referirme a una versión específica, he subido la versión actual al momento de escribir este artículo. Los números de sección, página y ecuación que utilizo se referirán a esa versión. Es una buena idea tenerlo abierto en una ventana diferente mientras lee este documento.
¿Por qué la EVM?
El Libro Amarillo original se escribió justo al comienzo del desarrollo de Ethereum. Describe el mecanismo de consenso original basado en prueba de trabajo (PoW) que se utilizó originalmente para asegurar la red. Sin embargo, Ethereum desactivó la prueba de trabajo y comenzó a utilizar el consenso basado en prueba de participación (PoS) en septiembre de 2022. Este tutorial se centrará en las partes del Libro Amarillo que definen la Máquina Virtual de Ethereum (EVM). La EVM no se vio afectada por la transición a la prueba de participación (excepto por el valor de retorno del código de operación DIFFICULTY).
9 Modelo de ejecución
Esta sección (pág. 12-14) incluye la mayor parte de la definición de la EVM.
El término estado del sistema incluye todo lo que necesita saber sobre el sistema para ejecutarlo. En una computadora típica, esto significa la memoria, el contenido de los registros, etc.
Una máquina de Turing (opens in a new tab) es un modelo computacional. Esencialmente, es una versión simplificada de una computadora, que ha demostrado tener la misma capacidad para ejecutar cálculos que una computadora normal (todo lo que una computadora puede calcular, una máquina de Turing lo puede calcular y viceversa). Este modelo facilita la demostración de varios teoremas sobre lo que es y lo que no es computable.
El término Turing completo (opens in a new tab) significa una computadora que puede ejecutar los mismos cálculos que una máquina de Turing. Las máquinas de Turing pueden entrar en bucles infinitos, y la EVM no puede porque se quedaría sin gas, por lo que solo es cuasi-Turing completa.
9.1 Conceptos básicos
Esta sección ofrece los conceptos básicos de la EVM y cómo se compara con otros modelos computacionales.
Una máquina de pila (opens in a new tab) es una computadora que almacena datos intermedios no en registros, sino en una pila (opens in a new tab). Esta es la arquitectura preferida para las máquinas virtuales porque es fácil de implementar, lo que significa que los errores y las vulnerabilidades de seguridad son mucho menos probables. La memoria en la pila se divide en palabras de 256 bits. Esto se eligió porque es conveniente para las operaciones criptográficas centrales de Ethereum, como el hashing Keccak-256 y los cálculos de curva elíptica. El tamaño máximo de la pila es de 1024 elementos (1024 x 256 bits). Cuando se ejecutan los códigos de operación, generalmente obtienen sus parámetros de la pila. Existen códigos de operación específicamente para reorganizar elementos en la pila, como POP (elimina el elemento de la parte superior de la pila), DUP_N (duplica el enésimo elemento en la pila), etc.
La EVM también tiene un espacio volátil llamado memoria que se utiliza para almacenar datos durante la ejecución. Esta memoria está organizada en palabras de 32 bytes. Todas las ubicaciones de memoria se inicializan en cero. Si ejecuta este código Yul (opens in a new tab) para agregar una palabra a la memoria, llenará 32 bytes de memoria rellenando el espacio vacío en la palabra con ceros, es decir, crea una palabra: con ceros en las ubicaciones 0-29, 0x60 en la 30 y 0xA7 en la 31.
mstore(0, 0x60A7)
mstore es uno de los tres códigos de operación que proporciona la EVM para interactuar con la memoria: carga una palabra en la memoria. Los otros dos son mstore8, que carga un solo byte en la memoria, y mload, que mueve una palabra de la memoria a la pila.
La EVM también tiene un modelo de almacenamiento no volátil separado que se mantiene como parte del estado del sistema: esta memoria está organizada en matrices de palabras (a diferencia de las matrices de bytes direccionables por palabras en la pila). Este almacenamiento es donde los contratos guardan datos persistentes: un contrato solo puede interactuar con su propio almacenamiento. El almacenamiento está organizado en asignaciones de clave-valor.
Aunque no se menciona en esta sección del Libro Amarillo, también es útil saber que existe un cuarto tipo de memoria. Los datos de llamada (calldata) son una memoria de solo lectura direccionable por bytes que se utiliza para almacenar el valor pasado con el parámetro data de una transacción. La EVM tiene códigos de operación específicos para administrar calldata. calldatasize devuelve el tamaño de los datos. calldataload carga los datos en la pila. calldatacopy copia los datos en la memoria.
La arquitectura de Von Neumann (opens in a new tab) estándar almacena código y datos en la misma memoria. La EVM no sigue este estándar por razones de seguridad: compartir memoria volátil hace posible cambiar el código del programa. En su lugar, el código se guarda en el almacenamiento.
Solo hay dos casos en los que el código se ejecuta desde la memoria:
- Cuando un contrato crea otro contrato (usando
CREATE(opens in a new tab) oCREATE2(opens in a new tab)), el código para el constructor del contrato proviene de la memoria. - Durante la creación de cualquier contrato, el código del constructor se ejecuta y luego regresa con el código del contrato real, también desde la memoria.
El término ejecución excepcional significa una excepción que hace que se detenga la ejecución del contrato actual.
9.2 Descripción general de las tarifas
Esta sección explica cómo se calculan las tarifas de gas. Hay tres costos:
Costo del código de operación
El costo inherente del código de operación específico. Para obtener este valor, busque el grupo de costos del código de operación en el Apéndice H (pág. 28, debajo de la ecuación (327)) y busque el grupo de costos en la ecuación (324). Esto le da una función de costo, que en la mayoría de los casos utiliza parámetros del Apéndice G (pág. 27).
Por ejemplo, el código de operación CALLDATACOPY (opens in a new tab) es miembro del grupo Wcopy. El costo del código de operación para ese grupo es Gverylow+Gcopy×⌈μs[2]÷32⌉. Al observar el Apéndice G, vemos que ambas constantes son 3, lo que nos da 3+3×⌈μs[2]÷32⌉.
Todavía necesitamos descifrar la expresión ⌈μs[2]÷32⌉. La parte más externa, ⌈ <value> ⌉ es la función techo, una función que, dado un valor, devuelve el número entero más pequeño que no sea menor que el valor. Por ejemplo, ⌈2.5⌉ = ⌈3⌉ = 3. La parte interna es μs[2]÷32. Al observar la sección 3 (Convenciones) en la pág. 3, μ es el estado de la máquina. El estado de la máquina se define en la sección 9.4.1 en la pág. 13. Según esa sección, uno de los parámetros del estado de la máquina es s para la pila. Juntándolo todo, parece que μs[2] es la ubicación n.º 2 en la pila. Al observar el código de operación (opens in a new tab), la ubicación n.º 2 en la pila es el tamaño de los datos en bytes. Al observar los otros códigos de operación en el grupo Wcopy, CODECOPY (opens in a new tab) y RETURNDATACOPY (opens in a new tab), también tienen un tamaño de datos en la misma ubicación. Por lo tanto, ⌈μs[2]÷32⌉ es el número de palabras de 32 bytes necesarias para almacenar los datos que se están copiando. Juntando todo, el costo inherente de CALLDATACOPY (opens in a new tab) es de 3 de gas más 3 por palabra de datos que se copian.
Costo de ejecución
El costo de ejecutar el código que estamos llamando.
- En el caso de
CREATE(opens in a new tab) yCREATE2(opens in a new tab), el constructor para el nuevo contrato. - En el caso de
CALL(opens in a new tab),CALLCODE(opens in a new tab),STATICCALL(opens in a new tab) oDELEGATECALL(opens in a new tab), el contrato que llamamos.
Costo de expansión de memoria
El costo de expandir la memoria (si es necesario).
En la ecuación 324, este valor se escribe como Cmem(μi')-Cmem(μi). Al observar la sección 9.4.1 nuevamente, vemos que μi es el número de palabras en la memoria. Por lo tanto, μi es el número de palabras en la memoria antes del código de operación y μi' es el número de palabras en la memoria después del código de operación.
La función Cmem se define en la ecuación 326: Cmem(a) = Gmemory × a + ⌊a2 ÷ 512⌋. ⌊x⌋ es la función piso, una función que, dado un valor, devuelve el número entero más grande que no sea mayor que el valor. Por ejemplo, ⌊2.5⌋ = ⌊2⌋ = 2. Cuando a < √512, a2 < 512, y el resultado de la función piso es cero. Por lo tanto, para las primeras 22 palabras (704 bytes), el costo aumenta linealmente con el número de palabras de memoria requeridas. Más allá de ese punto, ⌊a2 ÷ 512⌋ es positivo. Cuando la memoria requerida es lo suficientemente alta, el costo de gas es proporcional al cuadrado de la cantidad de memoria.
Tenga en cuenta que estos factores solo influyen en el costo de gas inherente: no tienen en cuenta el mercado de tarifas ni las propinas a los validadores que determinan cuánto debe pagar un usuario final; este es solo el costo bruto de ejecutar una operación particular en la EVM.
9.3 Entorno de ejecución
El entorno de ejecución es una tupla, I, que incluye información que no forma parte del estado de la cadena de bloques ni de la EVM.
| Parámetro | Código de operación para acceder a los datos | Código de Solidity para acceder a los datos |
|---|---|---|
| Ia | ADDRESS (opens in a new tab) | address(this) |
| Io | ORIGIN (opens in a new tab) | tx.origin |
| Ip | GASPRICE (opens in a new tab) | tx.gasprice |
| Id | CALLDATALOAD (opens in a new tab), etc. | msg.data |
| Is | CALLER (opens in a new tab) | msg.sender |
| Iv | CALLVALUE (opens in a new tab) | msg.value |
| Ib | CODECOPY (opens in a new tab) | address(this).code |
| IH | Campos del encabezado del bloque, como NUMBER (opens in a new tab) y DIFFICULTY (opens in a new tab) | block.number, block.difficulty, etc. |
| Ie | Profundidad de la pila de llamadas para llamadas entre contratos (incluida la creación de contratos) | |
| Iw | ¿Se le permite a la EVM cambiar de estado o se está ejecutando estáticamente? |
Son necesarios algunos otros parámetros para comprender el resto de la sección 9:
| Parámetro | Definido en la sección | Significado |
|---|---|---|
| σ | 2 (pág. 2, ecuación 1) | El estado de la cadena de bloques |
| g | 9.3 (pág. 13) | Gas restante |
| A | 6.1 (pág. 8) | Subestado acumulado (cambios programados para cuando finalice la transacción) |
| o | 9.3 (pág. 13) | Salida: el resultado devuelto en el caso de una transacción interna (cuando un contrato llama a otro) y llamadas a funciones de vista (cuando solo solicita información, por lo que no hay necesidad de esperar una transacción) |
9.4 Descripción general de la ejecución
Ahora que tenemos todos los preliminares, finalmente podemos comenzar a trabajar en cómo funciona la EVM.
Las ecuaciones 137-142 nos dan las condiciones iniciales para ejecutar la EVM:
| Símbolo | Valor inicial | Significado |
|---|---|---|
| μg | g | Gas restante |
| μpc | 0 | Contador de programa, la dirección de la siguiente instrucción a ejecutar |
| μm | (0, 0, ...) | Memoria, inicializada con todos ceros |
| μi | 0 | Ubicación de memoria más alta utilizada |
| μs | () | La pila, inicialmente vacía |
| μo | ∅ | La salida, conjunto vacío hasta y a menos que nos detengamos con datos de retorno (RETURN (opens in a new tab) o REVERT (opens in a new tab)) o sin ellos (STOP (opens in a new tab) o SELFDESTRUCT (opens in a new tab)). |
La ecuación 143 nos dice que hay cuatro condiciones posibles en cada momento durante la ejecución, y qué hacer con ellas:
Z(σ,μ,A,I). Z representa una función que prueba si una operación crea una transición de estado no válida (consulte detención excepcional). Si se evalúa como Verdadero, el nuevo estado es idéntico al anterior (excepto que se quema gas) porque los cambios no se han implementado.- Si el código de operación que se está ejecutando es
REVERT(opens in a new tab), el nuevo estado es el mismo que el estado anterior, se pierde algo de gas. - Si la secuencia de operaciones ha finalizado, como lo indica un
RETURN(opens in a new tab)), el estado se actualiza al nuevo estado. - Si no estamos en una de las condiciones finales 1-3, continúe ejecutando.
9.4.1 Estado de la máquina
Esta sección explica el estado de la máquina con mayor detalle. Especifica que w es el código de operación actual. Si μpc es menor que ||Ib||, la longitud del código, entonces ese byte (Ib[μpc]) es el código de operación. De lo contrario, el código de operación se define como STOP (opens in a new tab).
Como se trata de una máquina de pila (opens in a new tab), necesitamos realizar un seguimiento del número de elementos extraídos (δ) e insertados (α) por cada código de operación.
9.4.2 Detención excepcional
Esta sección define la función Z, que especifica cuándo tenemos una terminación anormal. Esta es una función booleana (opens in a new tab), por lo que utiliza ∨ para un o lógico (opens in a new tab) y ∧ para un y lógico (opens in a new tab).
Tenemos una detención excepcional si alguna de estas condiciones es verdadera:
-
μg < C(σ,μ,A,I) Como vimos en la sección 9.2, C es la función que especifica el costo de gas. No queda suficiente gas para cubrir el siguiente código de operación.
-
δw=∅ Si el número de elementos extraídos para un código de operación no está definido, entonces el código de operación en sí no está definido.
-
|| μs || < δw Desbordamiento inferior de la pila, no hay suficientes elementos en la pila para el código de operación actual.
-
w = JUMP ∧ μs[0]∉D(Ib) El código de operación es
JUMP(opens in a new tab) y la dirección no es unJUMPDEST(opens in a new tab). Los saltos solo son válidos cuando el destino es unJUMPDEST(opens in a new tab). -
w = JUMPI ∧ μs[1]≠0 ∧ μs[0] ∉ D(Ib) El código de operación es
JUMPI(opens in a new tab), la condición es verdadera (distinta de cero) por lo que el salto debería ocurrir, y la dirección no es unJUMPDEST(opens in a new tab). Los saltos solo son válidos cuando el destino es unJUMPDEST(opens in a new tab). -
w = RETURNDATACOPY ∧ μs[1]+μs[2]>|| μo || El código de operación es
RETURNDATACOPY(opens in a new tab). En este código de operación, el elemento de la pila μs[1] es el desplazamiento desde el que leer en el búfer de datos de retorno, y el elemento de la pila μs[2] es la longitud de los datos. Esta condición ocurre cuando intenta leer más allá del final del búfer de datos de retorno. Tenga en cuenta que no existe una condición similar para los datos de llamada o para el código en sí. Cuando intenta leer más allá del final de esos búferes, solo obtiene ceros. -
|| μs || - δw + αw > 1024
Desbordamiento de la pila. Si la ejecución del código de operación da como resultado una pila de más de 1024 elementos, se anula.
-
¬Iw ∧ W(w,μ) ¿Nos estamos ejecutando estáticamente (¬ es negación (opens in a new tab) e Iw es verdadero cuando se nos permite cambiar el estado de la cadena de bloques)? Si es así, y estamos intentando una operación de cambio de estado, no puede suceder.
La función W(w,μ) se define más adelante en la ecuación 150. W(w,μ) es verdadera si una de estas condiciones es verdadera:
-
w ∈ {CREATE, CREATE2, SSTORE, SELFDESTRUCT} Estos códigos de operación cambian el estado, ya sea creando un nuevo contrato, almacenando un valor o destruyendo el contrato actual.
-
LOG0≤w ∧ w≤LOG4 Si se nos llama estáticamente, no podemos emitir entradas de registro. Los códigos de operación de registro están todos en el rango entre
LOG0(A0) (opens in a new tab) yLOG4(A4) (opens in a new tab). El número después del código de operación de registro especifica cuántos temas contiene la entrada de registro. -
w=CALL ∧ μs[2]≠0 Puede llamar a otro contrato cuando es estático, pero si lo hace, no puede transferirle ETH.
-
-
w = SSTORE ∧ μg ≤ Gcallstipend No puede ejecutar
SSTORE(opens in a new tab) a menos que tenga más de Gcallstipend (definido como 2300 en el Apéndice G) de gas.
9.4.3 Validez del destino de salto
Aquí definimos formalmente cuáles son los códigos de operación JUMPDEST (opens in a new tab). No podemos simplemente buscar el valor de byte 0x5B, porque podría estar dentro de un PUSH (y por lo tanto ser datos y no un código de operación).
En la ecuación (153) definimos una función, N(i,w). El primer parámetro, i, es la ubicación del código de operación. El segundo, w, es el código de operación en sí. Si w∈[PUSH1, PUSH32] eso significa que el código de operación es un PUSH (los corchetes definen un rango que incluye los puntos finales). En ese caso, el siguiente código de operación está en i+2+(w−PUSH1). Para PUSH1 (opens in a new tab) necesitamos avanzar dos bytes (el PUSH en sí y el valor de un byte), para PUSH2 (opens in a new tab) necesitamos avanzar tres bytes porque es un valor de dos bytes, etc. Todos los demás códigos de operación de la EVM tienen solo un byte de longitud, por lo que en todos los demás casos N(i,w)=i+1.
Esta función se utiliza en la ecuación (152) para definir DJ(c,i), que es el conjunto (opens in a new tab) de todos los destinos de salto válidos en el código c, comenzando con la ubicación del código de operación i. Esta función se define de forma recursiva. Si i≥||c||, eso significa que estamos en o después del final del código. No vamos a encontrar más destinos de salto, así que simplemente devuelva el conjunto vacío.
En todos los demás casos, observamos el resto del código yendo al siguiente código de operación y obteniendo el conjunto a partir de él. c[i] es el código de operación actual, por lo que N(i,c[i]) es la ubicación del siguiente código de operación. DJ(c,N(i,c[i])) es, por lo tanto, el conjunto de destinos de salto válidos que comienza en el siguiente código de operación. Si el código de operación actual no es un JUMPDEST, simplemente devuelva ese conjunto. Si es JUMPDEST, inclúyalo en el conjunto de resultados y devuélvalo.
9.4.4 Detención normal
La función de detención H, puede devolver tres tipos de valores.
- Si no estamos en un código de operación de detención, devuelva ∅, el conjunto vacío. Por convención, este valor se interpreta como falso booleano.
- Si tenemos un código de operación de detención que no produce salida (ya sea
STOP(opens in a new tab) oSELFDESTRUCT(opens in a new tab)), devuelva una secuencia de bytes de tamaño cero como valor de retorno. Tenga en cuenta que esto es muy diferente del conjunto vacío. Este valor significa que la EVM realmente se detuvo, solo que no hay datos de retorno para leer. - Si tenemos un código de operación de detención que sí produce salida (ya sea
RETURN(opens in a new tab) oREVERT(opens in a new tab)), devuelva la secuencia de bytes especificada por ese código de operación. Esta secuencia se toma de la memoria, el valor en la parte superior de la pila (μs[0]) es el primer byte, y el valor posterior (μs[1]) es la longitud.
H.2 Conjunto de instrucciones
Antes de pasar a la subsección final de la EVM, 9.5, veamos las instrucciones en sí. Están definidas en el Apéndice H.2 que comienza en la pág. 29. Se espera que cualquier cosa que no se especifique como cambiante con ese código de operación específico permanezca igual. Las variables que sí cambian se especifican como <algo>′.
Por ejemplo, veamos el código de operación ADD (opens in a new tab).
| Valor | Mnemónico | δ | α | Descripción |
|---|---|---|---|---|
| 0x01 | ADD | 2 | 1 | Operación de suma. |
| μ′s[0] ≡ μs[0] + μs[1] |
δ es el número de valores que extraemos de la pila. En este caso dos, porque estamos sumando los dos valores superiores.
α es el número de valores que insertamos. En este caso uno, la suma.
Por lo tanto, la nueva parte superior de la pila (μ′s[0]) es la suma de la antigua parte superior de la pila (μs[0]) y el valor antiguo debajo de ella (μs[1]).
En lugar de repasar todos los códigos de operación con una "lista aburrida", este artículo explica solo aquellos códigos de operación que introducen algo nuevo.
| Valor | Mnemónico | δ | α | Descripción |
|---|---|---|---|---|
| 0x20 | KECCAK256 | 2 | 1 | Calcular el hash Keccak-256. |
| μ′s[0] ≡ KEC(μm[μs[0] . . . (μs[0] + μs[1] − 1)]) | ||||
| μ′i ≡ M(μi,μs[0],μs[1]) |
Este es el primer código de operación que accede a la memoria (en este caso, de solo lectura). Sin embargo, podría expandirse más allá de los límites actuales de la memoria, por lo que necesitamos actualizar μi. Hacemos esto usando la función M definida en la ecuación 328 en la pág. 29.
| Valor | Mnemónico | δ | α | Descripción |
|---|---|---|---|---|
| 0x31 | BALANCE | 1 | 1 | Obtener el saldo de la cuenta dada. |
| ... |
La dirección cuyo saldo necesitamos encontrar es μs[0] mod 2160. La parte superior de la pila es la dirección, pero debido a que las direcciones son de solo 160 bits, calculamos el valor módulo (opens in a new tab) 2160.
Si σ[μs[0] mod 2160] ≠ ∅, significa que hay información sobre esta dirección. En ese caso, σ[μs[0] mod 2160]b es el saldo de esa dirección. Si σ[μs[0] mod 2160] = ∅, significa que esta dirección no está inicializada y el saldo es cero. Puede ver la lista de campos de información de la cuenta en la sección 4.1 en la pág. 4.
La segunda ecuación, A'a ≡ Aa ∪ {μs[0] mod 2160}, está relacionada con la diferencia de costo entre el acceso al almacenamiento en caliente (almacenamiento al que se ha accedido recientemente y es probable que esté en caché) y el almacenamiento en frío (almacenamiento al que no se ha accedido y es probable que esté en un almacenamiento más lento que es más costoso de recuperar). Aa es la lista de direcciones a las que la transacción accedió previamente, a las que, por lo tanto, debería ser más barato acceder, como se define en la sección 6.1 en la pág. 8. Puede leer más sobre este tema en EIP-2929 (opens in a new tab).
| Valor | Mnemónico | δ | α | Descripción |
|---|---|---|---|---|
| 0x8F | DUP16 | 16 | 17 | Duplicar el 16º elemento de la pila. |
| μ′s[0] ≡ μs[15] |
Tenga en cuenta que para usar cualquier elemento de la pila, necesitamos extraerlo, lo que significa que también necesitamos extraer todos los elementos de la pila que están encima de él. En el caso de DUP<n> (opens in a new tab) y SWAP<n> (opens in a new tab), esto significa tener que extraer y luego insertar hasta dieciséis valores.
9.5 El ciclo de ejecución
Ahora que tenemos todas las partes, finalmente podemos comprender cómo se documenta el ciclo de ejecución de la EVM.
La ecuación (155) dice que dado el estado:
- σ (estado global de la cadena de bloques)
- μ (estado de la EVM)
- A (subestado, cambios que ocurrirán cuando finalice la transacción)
- I (entorno de ejecución)
El nuevo estado es (σ', μ', A', I').
Las ecuaciones (156)-(158) definen la pila y el cambio en ella debido a un código de operación (μs). La ecuación (159) es el cambio en el gas (μg). La ecuación (160) es el cambio en el contador de programa (μpc). Finalmente, las ecuaciones (161)-(164) especifican que los otros parámetros permanecen iguales, a menos que el código de operación los cambie explícitamente.
Con esto, la EVM está completamente definida.
Conclusión
La notación matemática es precisa y ha permitido que el Libro Amarillo especifique cada detalle de Ethereum. Sin embargo, tiene algunos inconvenientes:
- Solo puede ser entendida por humanos, lo que significa que las pruebas de cumplimiento (opens in a new tab) deben escribirse manualmente.
- Los programadores entienden el código informático. Pueden o no entender la notación matemática.
Tal vez por estas razones, las especificaciones más recientes de la capa de consenso (opens in a new tab) están escritas en Python. Hay especificaciones de la capa de ejecución en Python (opens in a new tab), pero no están completas. Hasta y a menos que todo el Libro Amarillo también se traduzca a Python o a un lenguaje similar, el Libro Amarillo continuará en servicio, y es útil poder leerlo.