Ethereum: de historia a labs
Artículo 9 de 48
19%
completado al llegar aquí
Sistemas distribuidosNota de investigación16 min de lectura
Estado, storage y Merkle Patricia Trie
Cómo Ethereum compromete cuentas y storage en una stateRoot de 32 bytes, cómo un slot de Solidity se vuelve clave del trie y cómo verificar con eth_getProof.
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 auditor pregunta: "¿qué saldo de token tenía la dirección A en el bloque 20 000 000?". Tu proveedor de RPC responde un número. ¿Cómo sabes que no se equivocó o mintió? La respuesta de Ethereum es que cada bloque compromete todo el estado en un solo hash de 32 bytes, y que cualquier dato puede acompañarse de una prueba que se verifica contra ese hash.
Este capítulo explica esa estructura. El estado es el conjunto de todas las cuentas con sus campos y el almacenamiento de cada contrato. Ethereum lo guarda en un Merkle Patricia Trie (MPT): un árbol de prefijos en el que cada nodo se identifica por el hash de su contenido. [1]
Brújula de lectura
Actor, evidencia y estado
Actor
Cliente de ejecuciónAplica cada bloque y recalcula las raíces; si no coinciden, rechaza el bloque.
Evidencia
stateRoot y pruebas MerkleUna ruta de nodos que une un valor con la raíz publicada en la cabecera.
Estado
Trie de cuentas y tries de storageUno global para cuentas y uno por contrato para sus slots.
Preguntas guía
- ¿Por qué basta un hash de 32 bytes en la cabecera para comprometer millones de cuentas?
- ¿Cómo se convierte una variable de Solidity en una clave del trie de storage?
- ¿Qué devuelve
eth_getProofy qué hay que tener para verificarlo? - ¿Qué datos de un contrato son realmente privados?
Conexiones dentro de la serie
- Antes: Una transacción Ethereum por dentro explica cómo una transacción produce un receipt.
- Después: EVM: stack, memory, storage y calldata muestra qué opcodes leen y escriben estos slots.
- Más adelante: Storage layout: donde Solidity cobra la factura profundiza en el empaquetado de variables, y Proxies y upgrades muestra qué pasa cuando dos contratos comparten storage.
Tres raíces en cada cabecera
Cada cabecera de bloque contiene tres raíces de tries distintos: stateRoot, transactionsRoot y receiptsRoot. [1] El yellow paper define stateRoot como el hash Keccak-256 del nodo raíz del trie de estado después de ejecutar todas las transacciones del bloque. [2]
Los tres tries no se comportan igual:
- Trie de estado. Hay uno solo y cambia con cada bloque. La clave de cada entrada es
keccak256(dirección)y el valor esrlp([nonce, balance, storageRoot, codeHash]). [1] - Tries de storage. Hay uno por cuenta. Su raíz es el campo
storageRootde la cuenta; para una cuenta sin código está vacío. [3] - Tries de transacciones y de receipts. Hay uno de cada tipo por bloque, con clave
rlp(índice). El de receipts nunca se actualiza después de crearse el bloque. [1]
El código de un contrato no está dentro del trie. La cuenta guarda solo codeHash y el código se busca en la base de datos por ese hash. [3]
De la cabecera del bloque a un slot de storage
Cómo leer esta figura
A la izquierda, la cabecera del bloque con stateRoot, transactionsRoot y receiptsRoot, y debajo los tries por bloque de transacciones y receipts, indexados por rlp del índice, que no cambian después. Una flecha azul rotulada stateRoot va a la raíz del trie de estado, que baja por un nodo branch, siguiendo los nibbles de la clave, hasta una hoja verde que representa una cuenta: clave keccak de la dirección y valor rlp de nonce, balance, storageRoot y codeHash. Desde la hoja, una flecha verde rotulada storageRoot sube a la raíz del trie de storage de ese contrato, que baja por un branch hasta una hoja de slot con clave keccak del slot y valor rlp de una palabra de 32 bytes. Otra flecha desde la cuenta, rotulada codeHash, apunta al código guardado aparte. Una nota dice que cambiar un solo slot cambia su hoja, cada nodo del camino, la storageRoot y la stateRoot.
Invariante del compromiso: por qué una raíz basta
Un árbol de Merkle enlaza nodos por el hash de su contenido. En el MPT, cuando un nodo referencia a otro, guarda keccak256(rlp(nodo)); si la codificación RLP del nodo hijo mide menos de 32 bytes, lo guarda directamente en lugar del hash. [1] [2]
De ahí sale el invariante que sostiene todo el capítulo: la raíz es una función determinista de todo el contenido. Si cambia un solo slot, cambia su hoja; eso cambia el hash que guarda su padre, y así hasta la raíz del trie de storage. Esa raíz es un campo de la cuenta, así que cambia la hoja de la cuenta y, siguiendo el camino, la stateRoot. [1]
La consecuencia práctica es fuerte. Dos clientes escritos en lenguajes distintos, con bases de datos internas distintas, están de acuerdo sobre el estado si y solo si calculan la misma stateRoot. Un cliente que recibe un bloque ejecuta las transacciones, recalcula la raíz y rechaza el bloque si no coincide con la de la cabecera. [2] Fabricar un estado distinto con la misma raíz exigiría encontrar una colisión de Keccak-256.
Traza de una búsqueda: branch, extension y leaf
La clave de una cuenta son 32 bytes, es decir 64 nibbles (grupos de 4 bits, un carácter hexadecimal cada uno). Un trie ingenuo tendría 64 niveles. El MPT comprime los tramos sin bifurcación con tres tipos de nodo: [1]
- Branch: una lista de 17 elementos. Las primeras 16 posiciones corresponden a los 16 valores posibles del siguiente nibble y la última guarda un valor si alguna clave termina ahí.
- Extension: un par
[camino parcial, referencia]que salta varios nibbles compartidos y apunta al siguiente nodo. - Leaf: un par
[resto del camino, valor]que termina la búsqueda.
Como extension y leaf son ambos pares, el primer nibble del camino parcial lleva una marca. Los valores 0 y 1 indican extension con longitud par o impar; 2 y 3, leaf con longitud par o impar. [1] Buscar una cuenta es, entonces, calcular keccak256(dirección), partirlo en nibbles y bajar desde la raíz: en cada branch se elige la posición del siguiente nibble, en cada extension se comprueba el prefijo y se salta, hasta llegar a una leaf.
Flujo visual
Buscar una cuenta en el trie de estado
Hashear la dirección antes de usarla como clave tiene un efecto útil: las claves quedan repartidas de forma uniforme, de modo que nadie puede fabricar direcciones que alarguen artificialmente un camino del trie.
Del slot de Solidity a la clave del trie
El storage de un contrato es, para la EVM, un mapa de claves de 32 bytes a valores de 32 bytes. Cada clave se llama slot. Solidity decide qué slot ocupa cada variable: las variables de estado se colocan una tras otra empezando por el slot 0, y varias variables pequeñas pueden compartir un slot. [4]
Los mappings no ocupan slots consecutivos. Si un mapping está declarado en el slot , el valor para la clave vive en , donde rellena los tipos de valor a 32 bytes. Para un array dinámico, el slot guarda la longitud y los elementos empiezan en . [4]
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
// Ilustrativo: un token mínimo sin lógica de transferencia.
contract Ledger {
mapping(address => uint256) public balanceOf; // slot 0 (la raíz del mapping)
uint256 public totalSupply; // slot 1
}El saldo de 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 en ese contrato está en el slot keccak256(dirección rellenada a 32 bytes ++ 0), que vale 0x14e04a66bf74771820a7400ff6cf065175b3d7eb25805a5bd1633b161af5d101. Ese es el número que se pasa a eth_getStorageAt. Dentro del trie de storage hay un segundo hash: la ruta real es keccak256(slot). [1] [3]
Separar esos dos hashes evita una confusión habitual. El primero lo calcula Solidity y define qué slot usa una variable. El segundo lo calcula el cliente y define dónde vive ese slot dentro del trie. Una herramienta de lectura solo necesita el primero.
Pruebas Merkle con eth_getProof: verificar sin confiar
EIP-1186 añadió el método eth_getProof. Recibe una dirección, una lista de slots y un bloque, y devuelve los campos de la cuenta, un accountProof con los nodos RLP desde la stateRoot hasta la hoja de la cuenta, y un storageProof por cada slot con los nodos desde la storageHash de la cuenta. [5] La especificación JSON-RPC actual mantiene ese método. [6]
Verificar una prueba es repetir la búsqueda con los nodos recibidos: hashear cada nodo, comprobar que coincide con la referencia del padre y llegar a la raíz. El punto delicado es de dónde sale la raíz. Si también la entrega el mismo proveedor, la prueba solo demuestra coherencia interna. La garantía aparece cuando la stateRoot viene de una cabecera que obtuviste por otra vía de confianza, por ejemplo de tu propio nodo o de un cliente ligero que sigue la cadena. [7]
Hay un límite operativo. Un nodo completo conserva el estado reciente, típicamente los últimos 128 bloques, y puede regenerar estados antiguos bajo demanda; un nodo de archivo guarda todo desde el génesis. [7] La pregunta del auditor sobre un bloque antiguo suele requerir un nodo de archivo o un proveedor que lo tenga.
Riesgo operativo: datos que parecen privados o borrados
Dos malentendidos aparecen en casi todas las revisiones de seguridad.
private no significa secreto. En Solidity, private e internal solo impiden que otros contratos lean o modifiquen la variable. El valor sigue siendo visible para todo el mundo fuera de la cadena. [8] Cualquiera puede leer cualquier slot con eth_getStorageAt. Una contraseña, una semilla o un dato personal guardado en storage es público.
SELFDESTRUCT ya no borra. Desde Dencun, EIP-6780 cambió el opcode: si se ejecuta en una transacción distinta a la que creó el contrato, solo transfiere el balance; el código, el storage y la cuenta permanecen. [9] [10] Un diseño que cuente con "destruir y volver a desplegar en la misma dirección" ya no funciona.
Leer storage también tiene un costo que depende de la historia de la transacción. EIP-2929 cobra 2100 gas por el primer acceso a un slot en una transacción (acceso frío) y 100 por los siguientes (acceso caliente). [11] [2] El capítulo de la EVM vuelve sobre esa diferencia.
Ejercicio con Anvil: leer un slot y su prueba
Este ejercicio despliega un contrato de 8 bytes escritos a mano. Su código de inicialización 0x602a5f5560015ff3 guarda 42 (0x2a) en el slot 0 y deja como código final un solo byte 0x00 (STOP). El capítulo siguiente desarma esos bytes opcode por opcode.
# Requiere Anvil corriendo (anvil) y la cuenta de prueba 0 de su mnemónica pública
export ETH_RPC_URL=http://127.0.0.1:8545
PK=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
cast send --private-key $PK --create 0x602a5f5560015ff3
# Si fue la primera transacción de esa cuenta en un Anvil recién arrancado:
C=0x5FbDB2315678afecb367f032d93F642f64180aa3
cast storage $C 0 # palabra de 32 bytes que termina en ...2a
cast code $C # 0x00
cast proof $C 0 # accountProof y storageProof en JSON
# Slot de un mapping declarado en el slot 0 para una clave de tipo address
cast index address 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 0Qué observar: cast storage devuelve el valor 42 relleno a 32 bytes; cast proof devuelve un storageHash igual a la raíz del trie de storage y una lista de nodos; cast index imprime el slot 0x14e04a66…d101 calculado arriba. Si ya enviaste otras transacciones, usa la dirección que aparece como contractAddress en el receipt. Detén Anvil para limpiar.
Bibliografía académica
- [1]
ethereum.org, "Merkle Patricia Trie," Ethereum developer documentation. [Online]. Available: https://ethereum.org/developers/docs/data-structures-and-encoding/patricia-merkle-trie/
Nodos branch, extension y leaf; tries de estado, storage, transacciones y receipts.
- [2]
G. Wood, "Ethereum: A secure decentralised generalised transaction ledger," Yellow Paper, Shanghai version efc5f9a, 2025.
Especificación formal de estado, transacciones, firma (apéndice F), gas y EVM.
- [3]
ethereum.org, "Ethereum accounts," Ethereum developer documentation. [Online]. Available: https://ethereum.org/developers/docs/accounts/
Tipos de cuenta, los cuatro campos de una cuenta y derivación de la dirección.
- [4]
Solidity Team, "Layout of state variables in storage and transient storage," Solidity documentation.
Slots, empaquetado, arrays y mappings en storage.
- [5]
S. Jentzsch and C. Jentzsch, "EIP-1186: RPC-method to get Merkle proofs - eth_getProof," Ethereum Improvement Proposals, 2018.
Pruebas Merkle de cuenta y storage por RPC.
- [6]
Ethereum, "Execution APIs: JSON-RPC specification," ethereum/execution-apis, GitHub repository.
Especificación OpenRPC de métodos eth_*, block tags, receipts y logs.
- [7]
ethereum.org, "Nodes and clients," Ethereum developer documentation. [Online]. Available: https://ethereum.org/developers/docs/nodes-and-clients/
Clientes de ejecución y consenso; nodos full, archive y light.
- [8]
Solidity Team, "Contracts: visibility and getters," Solidity documentation.
private e internal no ocultan datos fuera de la cadena.
- [9]
G. Ballet, V. Buterin and D. Feist, "EIP-6780: SELFDESTRUCT only in same transaction," Ethereum Improvement Proposals, 2023.
SELFDESTRUCT deja de borrar código y storage salvo en la transacción de creación.
- [10]
T. Beiko, "EIP-7569: Hardfork meta - Dencun," Ethereum Improvement Proposals, 2023.
EIPs de Dencun y activación en mainnet.
- [11]
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.
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.
Series relacionadas
HotStuff: de cero a estado del arte
Ruta progresiva para entender consenso BFT moderno: quórums, sincronía parcial, locks, taxonomía one/two/three-chain, QC/highQC, multipipeline, view-change, responsiveness, AggQC, Fast-HotStuff, sync y frontera DAG-BFT.
Substrate: del runtime al consenso propio
Serie de 20 capítulos, de neófito a experto, sobre Substrate (Polkadot SDK): runtime en Wasm, FRAME, extrinsics, estado, pesos, actualizaciones sin fork, Aura, BABE, GRANDPA, sincronización, parachains, XCM, Sassafras, BEEFY y cómo conectar un consenso BFT propio a un nodo.
Ruta práctica: activos digitales en la operación
Nueve módulos, de menor a mayor dependencia, para decidir cómo usar custodia, wallets, llaves, firmas multifirma y MPC, AML/KYT y stablecoins con Fireblocks, BitGo, Kaleido, Circle y Tether.
Labs para convertir lectura en práctica
Prueba de trabajo y reajuste de dificultad
Laboratorio determinista para medir el costo de la prueba de trabajo y ver cómo el reajuste por altura de bloque estabiliza el tiempo entre bloques.
Consensus benchmark
Banco reproducible para comparar consenso BFT y documentar los trade-offs de HotStuff-2.
Ethereum local: Anvil, wallet y primera transacción
Laboratorio base para levantar una red local, crear cuentas, firmar transacciones y observar receipts sin gastar ETH real.
RPC y Cast: leer Ethereum con lupa
Ejercicios con JSON-RPC y Cast para consultar bloques, nonces, gas, receipts, logs y errores reproducibles.
Sigue leyendo
Ideas conectadas con este artículo
Una ruta corta para continuar sin abrir veinte pestañas y perder el hilo.
EVM: stack, memory, storage y calldata
Modelo operativo de la Ethereum Virtual Machine: pila, memoria temporal, storage persistente, calldata y logs.
Storage layout: donde Solidity cobra la factura
Slots, packing, mappings, arrays, structs y por qué storage layout importa para gas y upgrades.
Ethereum: mapa de lectura antes de tocar Solidity
Ruta completa para aprender Ethereum desde historia y arquitectura hasta EVM, proof-of-stake, L2, seguridad, DeFi y labs reproducibles.
Antes de Ethereum: Bitcoin, scripts y la máquina mundial
Historia inicial de Ethereum: qué problema vio el whitepaper, por qué los contratos generales importaban y qué cambió frente a Bitcoin.