Ethereum: de historia a labs

Artículo 10 de 48

21%

completado al llegar aquí

Sistemas distribuidosNota de investigación16 min de lectura

EVM: stack, memory, storage y calldata

Dónde vive cada dato en la EVM, cuánto dura y cuánto cuesta: stack, memory, storage, transient storage, calldata y logs, con una traza de bytecode real.

Ruta Ethereum: 0/48

Prerequisitos recomendados

Si alguno de estos conceptos todavía está medio nebuloso, empieza por aquí. Te ahorra tropiezos y un par de cejas fruncidas.

Un contrato que funcionaba en pruebas empieza a costar diez veces más gas en producción. Al revisar, el equipo descubre que un bucle escribe en storage en cada vuelta, cuando bastaba con acumular en memoria y escribir una vez al final. El bug no está en la lógica, sino en no saber dónde vive cada dato. Este capítulo da ese mapa.

La Ethereum Virtual Machine (EVM) es la máquina que ejecuta el código de los contratos. Es una máquina de pila: las instrucciones toman sus operandos de una pila y dejan ahí el resultado, en lugar de usar registros con nombre. Cada elemento de la pila es una palabra de 256 bits, un tamaño elegido para manejar con comodidad hashes Keccak-256 y valores de secp256k1.

Brújula de lectura

Actor, evidencia y estado

Antes de avanzar, ubica quién actúa, qué prueba queda y qué parte del sistema cambia.

Actor

Bytecode del contrato

Cada opcode consume gas y mueve datos entre las regiones de la EVM.

Evidencia

Traza de opcodes y gasUsed

El receipt da el total; una traza muestra el costo de cada paso.

Estado

Solo storage persiste

Stack, memory y transient storage desaparecen; los logs quedan fuera del estado.

Preguntas guía

  • ¿Qué regiones de datos tiene la EVM y cuánto vive cada una?
  • ¿Por qué escribir un slot nuevo de storage cuesta miles de veces más que una suma?
  • ¿Qué cambia cuando un contrato llama a otro: qué se comparte y qué empieza de cero?
  • ¿Para qué sirve transient storage y en qué se diferencia de memory?

Conexiones dentro de la serie

Seis lugares para un dato

Cada vez que un contrato recibe una llamada, la EVM crea un marco de ejecución: un contexto con su propio contador de programa, su gas disponible y sus regiones de trabajo. Los datos pueden estar en seis lugares:

RegiónQué esCuánto viveCosto típico
Stackpila de hasta 1024 palabras de 256 bitsel marco actual2 o 3 gas por operación simple
Memoryarreglo de bytes, empieza en ceroel marco actual3 gas por palabra más un término cuadrático
Calldatabytes de entrada de la llamada, solo lecturael marco actual3 gas por CALLDATALOAD
Storagemapa persistente de 32 a 32 bytes por contratoentre transacciones2100 frío, 100 caliente; 20 000 al pasar de cero a no cero
Transient storagemapa de 32 a 32 bytes por contratola transacción100 gas por lectura o escritura
Logsregistros emitidos con LOG0 a LOG4quedan en el receiptsegún topics y bytes

El tope de 1024 elementos de pila y los costos de memoria y storage están en el yellow paper; los de transient storage, en EIP-1153; la tabla de opcodes de ethereum.org los resume. Los accesos fríos y calientes a storage vienen de EIP-2929: el primer acceso a un slot dentro de una transacción es frío y los siguientes son calientes.

Figura 1

Cuánto vive cada región de datos de la EVM

Fuentes:

[2][3]
Cuánto vive cada región de datos de la EVM
Cómo leer esta figura

Eje horizontal de tiempo con dos transacciones. En la transacción 1, el marco A llama al marco B, que ocupa una franja central. Fila calldata y fila stack y memory: barras azules para el marco A antes y después de la franja, y una barra violeta distinta para el marco B, que empieza vacío. Fila transient storage: una barra verde continua durante toda la transacción 1 que termina con el rótulo se borra, y una barra nueva en la transacción 2. Fila storage: una barra ocre continua que atraviesa ambas transacciones. Fila logs: un punto LOG en B y otro LOG en A, con una flecha hacia el receipt, si no revierte.

Visual original de TrautsLab. Calldata, stack y memory pertenecen a cada marco de llamada; transient storage dura una transacción completa; storage sobrevive a las transacciones; los logs salen hacia el receipt solo si la ejecución no revierte.

Traza de ejecución: ocho bytes que escriben un slot

El capítulo anterior desplegó el código de inicialización 0x602a5f5560015ff3. Leído byte a byte con la tabla de opcodes, son seis instrucciones. PUSH0 (0x5f) existe desde EIP-3855 y empuja un cero sin bytes de datos.

Byte(s)OpcodePila después (cima a la izquierda)Gas
60 2aPUSH1 0x2a[42]3
5fPUSH0[0, 42]2
55SSTORE[]: slot 0 pasa de 0 a 4222 100
60 01PUSH1 0x01[1]3
5fPUSH0[0, 1]2
f3RETURNdevuelve memory[0:1] = 0x003 por expandir memoria

SSTORE toma de la cima la clave y debajo el valor. Su costo aquí suma dos partes del yellow paper: 2100 por acceso frío al slot y 20 000 por pasar un valor de cero a no cero. RETURN toma un desplazamiento y una longitud y devuelve ese tramo de memoria; en una creación, esos bytes se convierten en el código del contrato. Como la memoria empieza en cero, el código resultante es el byte 0x00, que es STOP.

La lección de la traza es de proporción. Cinco de las seis instrucciones cuestan entre 2 y 3 gas; la escritura en storage cuesta más de siete mil veces eso. Cualquier optimización seria empieza por contar escrituras de storage.

Memory y calldata: lo que se borra al terminar la llamada

Calldata son los bytes que acompañan a la llamada. El contrato los lee con CALLDATALOAD, que copia 32 bytes desde una posición a la pila, o con CALLDATACOPY, que los copia a memoria. No se pueden modificar.

Memory es un arreglo de bytes que empieza en cero en cada marco y crece a medida que se usa. Su costo no es lineal: para aa palabras de 32 bytes, el yellow paper cobra 3a+⌊a2/512⌋3a + \lfloor a^2/512 \rfloor gas en total, y cada expansión paga la diferencia. Usar unos pocos kilobytes es barato; usar megabytes se vuelve prohibitivo, y ese término cuadrático es lo que acota la memoria de un marco en la práctica.

Solidity organiza la memoria con una convención propia: los bytes 0x00 a 0x3f son espacio temporal para hashes, 0x40 guarda el puntero de memoria libre y 0x60 es una palabra que siempre vale cero. Los objetos nuevos empiezan en 0x80. Un bloque assembly que escribe en memoria sin respetar ese puntero puede pisar datos que el compilador cree intactos.

Storage y transient storage: lo que sobrevive a la llamada

Storage es el mapa persistente de cada contrato que vimos dentro del trie. Es el único lugar donde un dato sobrevive a la transacción. Además del costo de escritura, cada lectura fría cuesta 2100 gas. En un bucle que lee el mismo slot en cada vuelta, la primera lectura es fría y las demás cuestan 100 gas cada una; copiar el valor a una variable local antes del bucle deja una sola lectura de storage y convierte el resto en operaciones de pila de 2 o 3 gas.

Transient storage se añadió con EIP-1153 en Dencun, activado en mainnet el 13 de marzo de 2024. Tiene la forma de storage, un mapa de 32 a 32 bytes por contrato, pero se descarta al final de cada transacción. Se accede con TSTORE (0x5d) y TLOAD (0x5c) por 100 gas.

La diferencia con memory es el alcance. La memoria es privada de cada marco: si A llama a B y B vuelve a llamar a A, el segundo A empieza con memoria vacía. El transient storage, en cambio, se comparte entre todas las llamadas internas de la misma transacción. Eso lo hace útil para un cerrojo contra reentrada o para pasar un valor entre llamadas sin pagar storage. El propio EIP advierte que, si los datos solo deben vivir dentro de un marco, conviene usar memory.

Decisión de diseño

Dónde guardar un dato según cuánto debe vivir

Reglas de decisión rápida

  • Solo dentro de esta función

    Variable temporal, buffer, resultado intermedio.

    → Stack o memory

  • Entre llamadas internas de una misma transacción

    Cerrojo de reentrada, contexto compartido entre callbacks.

    → Transient storage (TSTORE/TLOAD)

  • Entre transacciones

    Saldos, configuración, propiedad.

    → Storage, minimizando escrituras

  • Solo para observadores fuera de la cadena

    Historial que un indexador o una UI necesita leer.

    → Eventos (logs), nunca como fuente de verdad del contrato

Elige la región más barata que cumpla el tiempo de vida que necesitas.

Logs y llamadas entre contratos: invariantes a respetar

Los logs son registros que el contrato emite con LOG0 a LOG4: hasta cuatro topics de 32 bytes, que sirven para filtrar, y un bloque de datos. Quedan en el receipt, no en el estado. Ningún contrato puede leerlos, ni siquiera el que los emitió. Por eso un evento es una notificación para el mundo exterior, no una forma de guardar información para el propio contrato.

Cuando un contrato llama a otro con CALL, se abre un marco nuevo con stack, memory y calldata propios, y el código llamado trabaja sobre su propio storage. DELEGATECALL es la excepción importante: ejecuta el código de otro contrato sobre el storage del que llama. Ese es el mecanismo de los proxies y la razón por la que un cambio de layout entre versiones puede corromper datos.

Si un marco revierte, se descartan sus cambios de storage, de transient storage y sus logs, y el que llamó recibe un indicador de fallo que debe comprobar. Dos límites más acotan lo que un contrato puede ser: la pila no puede superar 1024 elementos, o la ejecución se detiene con error, y el código desplegado no puede superar 24 576 bytes.

Riesgo operativo: costos que cambian con el contexto

El mismo código no siempre cuesta lo mismo. Una lectura de storage cuesta 2100 gas si es el primer acceso al slot en la transacción y 100 si no lo es. Una estimación de gas hecha con una transacción aislada puede quedarse corta cuando esa llamada forma parte de un lote, o sobrar cuando otro paso ya calentó el slot.

Tres prácticas reducen sorpresas: estimar con el mismo contexto en que se va a ejecutar, dejar margen en gas_limit, y revisar en cada auditoría de gas cuántos SSTORE hace cada camino de código. Los números concretos cambian con los upgrades, así que conviene leerlos en el EIP vigente y no en una tabla copiada hace años.

Taller de bytecode: desensamblar y repetir la traza

Este taller reutiliza el contrato de 8 bytes. Todos los comandos se ejecutan contra Anvil, el nodo local de Foundry, con su cuenta de prueba pública.

Bash
# Desensamblar el código de inicialización (no necesita nodo)
cast disassemble 0x602a5f5560015ff3
 
# Desplegarlo en Anvil y leer el slot que escribió
export ETH_RPC_URL=http://127.0.0.1:8545
PK=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
cast send --private-key $PK --create 0x602a5f5560015ff3
cast storage <DIRECCION_DEL_CONTRATO> 0
 
# Pedir la traza paso a paso de la transacción de despliegue (método de depuración que Anvil expone)
cast rpc debug_traceTransaction <TX_HASH>

Qué observar: el desensamblado debe listar las seis instrucciones de la tabla; la traza debe mostrar SSTORE como el paso más caro, con 42 y 0 en la pila justo antes de ejecutarse. debug_traceTransaction no forma parte de la especificación estándar eth_*; es un método de depuración que ofrecen algunos clientes y Anvil, y su formato de salida puede variar entre implementaciones.

Referencias

Bibliografía académica

Formato IEEENumerado para ingeniería, sistemas y protocolos.
  1. [1]

    ethereum.org, "Ethereum Virtual Machine (EVM)," Ethereum developer documentation. [Online]. Available: https://ethereum.org/developers/docs/evm/

    official docsID: ETH-EVM-DOCSAbrir fuente (abre en otra pestaña)

    Máquina de pila, memoria y transient storage.

  2. [2]

    G. Wood, "Ethereum: A secure decentralised generalised transaction ledger," Yellow Paper, Shanghai version efc5f9a, 2025.

    formal specID: ETH-YELLOWPAPERAbrir fuente (abre en otra pestaña)

    Especificación formal de estado, transacciones, firma (apéndice F), gas y EVM.

  3. [3]

    A. Akhunov and M. Salem, "EIP-1153: Transient storage opcodes," Ethereum Improvement Proposals, 2018.

    TSTORE y TLOAD: almacenamiento que se descarta al final de la transacción.

  4. [4]

    ethereum.org, "Opcodes for the EVM," Ethereum developer documentation. [Online]. Available: https://ethereum.org/developers/docs/evm/opcodes/

    official docsID: ETH-OPCODES-DOCSAbrir fuente (abre en otra pestaña)

    Tabla de opcodes con entradas, salidas y gas.

  5. [5]

    V. Buterin and M. Swende, "EIP-2929: Gas cost increases for state access opcodes," Ethereum Improvement Proposals, 2020.

    Accesos fríos y calientes a cuentas y slots.

  6. [6]

    A. Beregszaszi, H. De la cruz and P. Bylica, "EIP-3855: PUSH0 instruction," Ethereum Improvement Proposals, 2021.

    Opcode 0x5f que empuja cero.

  7. [7]

    Solidity Team, "Layout in memory," Solidity documentation.

    official docsID: SOLIDITY-MEMORY-LAYOUTAbrir fuente (abre en otra pestaña)

    Scratch space, free memory pointer y zero slot.

  8. [8]

    T. Beiko, "EIP-7569: Hardfork meta - Dencun," Ethereum Improvement Proposals, 2023.

    EIPs de Dencun y activación en mainnet.

  9. [9]

    Solidity Team, "Contracts: events," Solidity documentation.

    official docsID: SOLIDITY-EVENTSAbrir fuente (abre en otra pestaña)

    Logs inaccesibles desde contratos y hasta tres parámetros indexed.

  10. [10]

    V. Buterin, "EIP-170: Contract code size limit," Ethereum Improvement Proposals, 2016.

    Límite de 24576 bytes de código desplegado.

Fuentes primarias, papers seminales y especificaciones técnicas citadas en esta investigación.

Rutas vecinas

Si este tema te abrió hambre, sigue por aquí.

Series y labs que comparten área o dependen de esta ruta. Pocas opciones, para no convertir la curiosidad en menú infinito.

Sigue leyendo

Ideas conectadas con este artículo

Una ruta corta para continuar sin abrir veinte pestañas y perder el hilo.

Los comentarios se cargan al final de la lectura para no meter peso antes de tiempo.