Ir al contenido principal

Explicación de las especificaciones de la EVM del Yellow Paper

evm
Intermedio
qbzzt
15 de mayo de 2022
21 minuto leído

El Yellow Paperopens in a new tab es la especificación formal de Ethereum. Excepto donde se modifique por el proceso de EIP, contiene la descripción exacta de cómo funciona todo. Está escrito como un documento matemático, que incluye terminología con la que los programadores podrían no estar familiarizados. En este documento aprenderá a leerlo y, por extensión, otros documentos matemáticos relacionados.

¿Qué Yellow Paper?

Como casi todo en Ethereum, el Yellow Paper evoluciona con el tiempo. Para poder hacer referencia a una versión específica, he subido la versión actual en el momento de la redacción. 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 Yellow Paper original se escribió justo al principio del desarrollo de Ethereum. Describe el mecanismo de consenso original basado en la prueba de trabajo que se utilizó originalmente para asegurar la red. Sin embargo, Ethereum dejó de usar la prueba de trabajo y comenzó a utilizar el consenso basado en la prueba de participación en septiembre de 2022. Este tutorial se centrará en las partes del Yellow Paper que definen la Máquina Virtual de Ethereum. La EVM no resultó modificada por la transición a la prueba de participación (a excepción del valor de retorno del código de operación DIFFICULTY).

9 Modelo de ejecución

Esta sección (p. 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 Turingopens in a new tab es un modelo computacional. Esencialmente, es una versión simplificada de una computadora que, según se ha probado, cuenta con la misma capacidad de realizar cálculos que una computadora normal (todo lo que una computadora puede calcular una máquina de Turing puede calcular y viceversa). Este modelo facilita probar varios teoremas sobre qué es y qué no es computable.

El término Turing-completeopens 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 es solo cuasi-Turing-complete.

9.1 Conceptos básicos

Esta sección proporciona los fundamentos básicos de la EVM y cómo se compara con otros modelos computacionales.

Una máquina de pilaopens in a new tab es una computadora que almacena datos intermedios no en registros, sino en una pilaopens 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 hash Keccak-256 y los cómputos 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, normalmente obtienen sus parámetros de la pila. Hay códigos de operación específicos para reorganizar elementos en la pila, tales 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 a cero. Si ejecuta este código de Yulopens in a new tab para agregar una palabra a la memoria, llenará 32 bytes de memoria rellenando el espacio vacío de 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.

1mstore(0, 0x60A7)

mstore es uno de los tres códigos de operación que la EVM proporciona 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 se organiza 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 se organiza en asignaciones de clave-valor.

Aunque no se menciona en esta sección del Yellow Paper, también es útil saber que existe un cuarto tipo de memoria. Calldata es 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 gestionar 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 Neumannopens in a new tab estándar almacena el código y los 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 CREATEopens in a new tab o CREATE2opens in a new tab), el código del constructor del contrato proviene de la memoria.
  • Durante la creación de cualquier contrato, el código del constructor se ejecuta y luego devuelve el código del contrato real, también desde la memoria.

El término ejecución excepcional significa una excepción que provoca que la ejecución del contrato actual se detenga.

9.2 Resumen 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 costo del código de operación en el Apéndice H (p. 28, bajo la ecuación (327)) y busque el grupo de costo en la ecuación (324). Esto le proporcionará una función de costo, que en la mayoría de los casos utiliza parámetros del Apéndice G (p. 27).

Por ejemplo, el código de operación CALLDATACOPYopens in a new tab es un miembro del grupo Wcopy. El costo del código de operación para ese grupo es Gverylow+Gcopy×⌈μs[2]÷32⌉. Al mirar 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, ⌈ <valor> ⌉ es la función techo, una función que, dado un valor, devuelve el entero más pequeño que no es menor que el valor. Por ejemplo, ⌈2.5⌉ = ⌈3⌉ = 3. La parte interna es μs[2]÷32. Al mirar la sección 3 (Convenciones) en la p. 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. 13. Según esa sección, uno de los parámetros de estado de la máquina es s para la pila. En conjunto, parece que μs[2] es la ubicación n.º 2 en la pila. Al observar el código de operaciónopens 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 del grupo Wcopy, CODECOPYopens in a new tab y RETURNDATACOPYopens in a new tab, también tienen un tamaño de datos en la misma ubicación. Así que ⌈μs[2]÷32⌉ es el número de palabras de 32 bytes necesarias para almacenar los datos que se están copiando. En resumen, el coste inherente de CALLDATACOPYopens in a new tab es de 3 de gas más 3 por cada palabra de datos que se copia.

Costo de ejecución

El costo de ejecutar el código al que estamos llamando.

Costo de expansión de la memoria

El costo de expandir la memoria (si es necesario).

En la ecuación 324, este valor se escribe como Cmemi')-Cmemi). Mirando de nuevo la sección 9.4.1, 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 suelo, una función que, dado un valor, devuelve el entero más grande que no es mayor que el valor. Por ejemplo, ⌊2.5⌋ = ⌊2⌋ = 2. Cuando a < √512, a2 < 512, y el resultado de la función suelo es cero. Así, 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.

Nota: estos factores solo influyen en el costo inherente del gas; 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.

Lea más sobre el gas.

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ámetroCódigo de operación para acceder a los datosCódigo de Solidity para acceder a los datos
IaADDRESSopens in a new tabaddress(this)
IoORIGINopens in a new tabtx.origin
IpGASPRICEopens in a new tabtx.gasprice
IdCALLDATALOADopens in a new tab, etc.msg.data
IsCALLERopens in a new tabmsg.sender
IvCALLVALUEopens in a new tabmsg.value
IbCODECOPYopens in a new tabaddress(this).code
IHCampos de la cabecera del bloque, como NUMBERopens in a new tab y DIFFICULTYopens in a new tabblock.number, block.difficulty, etc.
IeProfundidad 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 de forma estática?

Algunos otros parámetros son necesarios para entender el resto de la sección 9:

ParámetroDefinido en la secciónSignificado
σ2 (p. 2, ecuación 1)El estado de la cadena de bloques
g9.3 (p. 13)Gas restante
A6.1 (p. 8)Subestado acumulado (cambios programados para cuando finalice la transacción)
o9.3 (p. 13)Salida: el resultado devuelto en el caso de una transacción interna (cuando un contrato llama a otro) y llamadas a funciones de visualización (cuando solo está pidiendo información, por lo que no hay necesidad de esperar una transacción).

9.4 Resumen de la ejecución

Ahora que tenemos todos los preliminares, podemos finalmente empezar a ver cómo funciona la EVM.

Las ecuaciones 137-142 nos dan las condiciones iniciales para ejecutar la EVM:

SímboloValor inicialSignificado
μggGas restante
μpc0Contador de programa, la dirección de la siguiente instrucción a ejecutar
μm(0, 0, ...)Memoria, inicializada a todo ceros
μi0Ubicación de memoria más alta utilizada
μs()La pila, inicialmente vacía
μoLa salida, conjunto vacío hasta y a menos que nos detengamos con datos de retorno (RETURNopens in a new tab o REVERTopens in a new tab) o sin ellos (STOPopens in a new tab o SELFDESTRUCTopens 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:

  1. Z(σ,μ,A,I). Z representa una función que comprueba si una operación crea una transición de estado no válida (véase detención excepcional). Si se evalúa como Verdadero, el nuevo estado es idéntico al antiguo (excepto que se quema gas) porque los cambios no se han implementado.
  2. Si el código de operación que se está ejecutando es REVERTopens in a new tab, el nuevo estado es el mismo que el estado antiguo y se pierde algo de gas.
  3. Si la secuencia de operaciones ha finalizado, como indica un RETURNopens in a new tab), el estado se actualiza al nuevo estado.
  4. Si no estamos en una de las condiciones finales 1-3, se continúa la ejecución.

9.4.1 Estado de la máquina

Esta sección explica el estado de la máquina con más 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 (Ibpc]) es el código de operación. De lo contrario, el código de operación se define como STOPopens in a new tab.

Como se trata de una máquina de pilaopens in a new tab, necesitamos hacer un seguimiento del número de elementos extraídos (δ) e introducidos (α) 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 booleanaopens in a new tab, por lo que utiliza para un O lógicoopens in a new tab y para un Y lógicoopens 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 propio código de operación no está definido.

  • || μs || < δw Subdesbordamiento de 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 JUMPopens in a new tab y la dirección no es un JUMPDESTopens in a new tab. Los saltos solo son válidos cuando el destino es un JUMPDESTopens in a new tab.

  • w = JUMPI ∧ μs[1]≠0 ∧ μs[0] ∉ D(Ib) El código de operación es JUMPIopens 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 un JUMPDESTopens in a new tab. Los saltos solo son válidos cuando el destino es un JUMPDESTopens in a new tab.

  • w = RETURNDATACOPY ∧ μs[1]+μs[2]>|| μo || El código de operación es RETURNDATACOPYopens in a new tab. En este código de operación, el elemento de la pila μs[1] es el desplazamiento desde el que se lee 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 se produce cuando se intenta leer más allá del final del búfer de datos de retorno. Tenga en cuenta que no hay una condición similar para calldata o para el propio código. Cuando intenta leer más allá del final de esos búferes, solo obtiene ceros.

  • || μs || - δw + αw > 1024

    Desbordamiento de pila. Si la ejecución del código de operación da como resultado una pila de más de 1024 elementos, se aborta.

  • ¬Iw ∧ W(w,μ) ¿Estamos ejecutando de forma estática (¬ es negaciónopens 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 que cambia el estado, no puede ocurrir.

    La función W(w,μ) se define más adelante en la ecuación 150. W(w,μ) es verdadero 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 de forma estática, 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 y LOG4 (A4)opens in a new tab. El número después del código de operación del registro especifica cuántos temas contiene la entrada del registro.

    • w=CALL ∧ μs[2]≠0 Puede llamar a otro contrato cuando está en modo estático, but si lo hace no puede transferirle ETH.

  • w = SSTORE ∧ μg ≤ Gcallstipend No puede ejecutar SSTOREopens 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 del salto

Aquí definimos formalmente qué son los códigos de operación JUMPDESTopens 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 propio código de operación. Si w∈[PUSH1, PUSH32] 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 PUSH1opens in a new tab necesitamos avanzar dos bytes (el propio PUSH y el valor de un byte), para PUSH2opens 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 conjuntoopens in a new tab de todos los destinos de salto válidos en el código c, comenzando en la ubicación del código de operación i. Esta función se define de forma recursiva. Si i≥||c||, significa que estamos al final del código o después. No vamos a encontrar más destinos de salto, así que simplemente devolvemos el conjunto vacío.

En todos los demás casos, miramos el resto del código yendo al siguiente código de operación y obteniendo el conjunto que comienza 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, devuelve , el conjunto vacío. Por convención, este valor se interpreta como Booleano falso.
  • Si tenemos un código de operación de detención que no produce una salida (ya sea STOPopens in a new tab o SELFDESTRUCTopens in a new tab), devuelve una secuencia de cero bytes de tamaño 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 produce una salida (ya sea RETURNopens in a new tab o REVERTopens in a new tab), devuelve 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 que le sigue (μ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í. Se definen en el Apéndice H.2, que comienza en la p. 29. Se espera que todo lo que no se especifique que cambie 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 ADDopens in a new tab.

ValorMnemónicoδαDescripción
0x01ADD21Operació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 reintroducimos. En este caso uno, la suma.

Por lo tanto, el nuevo tope de la pila (μ′s[0]) es la suma del antiguo tope de la pila (μs[0]) y el valor antiguo debajo de él (μs[1]).

En lugar de repasar todos los códigos de operación con una "lista que hace que se pongan los ojos en blanco", este artículo explica solo aquellos códigos de operación que introducen algo nuevo.

ValorMnemónicoδαDescripción
0x20KECCAK25621Calcular el hash Keccak-256.
μ′s[0] ≡ KEC(μms[0] . . . (μs[0] + μs[1] − 1)])
μ′i ≡ M(μis[0],μs[1])

Este es el primer código de operación que accede a la memoria (en este caso, solo de 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. 29.

ValorMnemónicoδαDescripción
0x31BALANCE11Obtener el saldo de la cuenta dada.
...

La dirección cuyo saldo necesitamos encontrar es μs[0] mod 2160. El tope de la pila es la dirección, pero como las direcciones son de solo 160 bits, calculamos el valor móduloopens 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 para 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 de la p. 4.

La segunda ecuación, A'a ≡ Aa ∪ {μs[0] mod 2160}, está relacionada con la diferencia en costo entre el acceso al almacenamiento caliente (almacenamiento al que se ha accedido recientemente y es probable que esté almacenado en caché) y el almacenamiento frío (almacenamiento al que no se ha accedido y es probable que esté en almacenamiento más lento que es más caro de recuperar). Aa es la lista de direcciones a las que la transacción ha accedido previamente, por lo que debería ser más barato acceder a ellas, como se define en la sección 6.1 en la p. 8. Puede leer más sobre este tema en EIP-2929opens in a new tab.

ValorMnemónicoδαDescripción
0x8FDUP161617Duplicar el 16.º elemento de la pila.
μ′s[0] ≡ μs[15]

Tenga en cuenta que para usar cualquier elemento de la pila, necesitamos extraerlo (pop), lo que significa que también necesitamos extraer todos los elementos de la pila que están por 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 (pop) y luego reintroducir (push) hasta dieciséis valores.

9.5 El ciclo de ejecución

Ahora que tenemos todas las partes, finalmente podemos entender 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 la transacción termine)
  • 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 demás parámetros permanecen igual, 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 Yellow Paper especifique cada detalle de Ethereum. Sin embargo, tiene algunas desventajas:

  • Solo puede ser entendido por humanos, lo que significa que las pruebas de cumplimientoopens in a new tab deben escribirse manually.
  • Los programadores entienden el código informático. Pueden o no entender la notación matemática.

Quizás por estas razones, las especificaciones de la capa de consensoopens in a new tab más nuevas están escritas en Python. Hay especificaciones de la capa de ejecución en Pythonopens in a new tab, pero no están completas. Hasta que todo el Yellow Paper se traduzca también a Python o a un lenguaje similar, el Yellow Paper seguirá en servicio, y es útil poder leerlo.

Última actualización de la página: 21 de octubre de 2025

¿Le ha resultado útil este tutorial?