eth_getStorageAt devuelve la palabra cruda de 32 bytes almacenada en un slot dado, pero Solidity empaqueta varias variables en un solo slot y deriva los slots de elementos de mappings y arrays dinámicos con keccak256, por lo que una lectura ingenua del slot 0 a menudo devuelve un valor inesperado. Este artículo explica las reglas de layout documentadas en la documentación de Solidity, muestra cómo calcular slots por orden de declaración y slots derivados, y proporciona ejemplos ejecutables en Node.js que aplican máscaras y desplazamientos a valores empaquetados. También cubre la decodificación de bools, direcciones, enteros con signo y cadenas largas, la verificación de una decodificación contra una función view pública con eth_call, el riesgo del layout de almacenamiento en proxies y el compromiso frente a eth_getProof. El objetivo es convertir la aritmética de slots de una adivinanza en un método verificado y reproducible.
Por qué una lectura ingenua del slot 0 devuelve datos inesperados
La especificación JSON-RPC de Ethereum define eth_getStorageAt como un método que toma una cantidad de posición de 32 bytes, una etiqueta de bloque y devuelve una palabra de datos de 32 bytes. No devuelve un valor tipado, un nombre de variable ni ningún delimitador que indique dónde termina una variable de Solidity y comienza la siguiente. El método es una ventana cruda al almacenamiento del contrato, no un getter consciente de campos.
El layout documentado de Solidity para las variables de estado en el almacenamiento coloca las variables de estado en slots de 32 bytes en orden de declaración, y cuando varios valores caben se empaquetan en un solo slot comenzando desde los bits de orden inferior. Esto significa que la variable que buscas puede compartir el slot 0 con varias otras, por lo que la palabra devuelta debe enmascararse y desplazarse en lugar de leerse como un número.
Esta es la fuente de confusión más común: un desarrollador lee el slot 0, ve un entero grande y asume que el endpoint RPC está equivocado. En la práctica, el endpoint devolvió exactamente lo que el contrato almacenó; el lector simplemente no tuvo en cuenta el empaquetado. Las reglas autoritativas están en la documentación de Solidity en https://docs.soliditylang.org/en/latest/internals/layout_in_storage.html y la semántica del método está en la especificación JSON-RPC de Ethereum en https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_getstorageat.
- eth_getStorageAt devuelve una cadena hexadecimal de 32 bytes, nunca un valor de Solidity decodificado.
- El empaquetado significa que un slot puede contener varias variables, ordenadas desde el extremo de orden inferior.
- No hay delimitador de campo en la respuesta, por lo que la decodificación es tu responsabilidad.
La regla de empaquetado explicada operativamente
La documentación de Solidity sobre el layout de variables de estado en el almacenamiento especifica la regla de empaquetado. Los valores se empaquetan desde el extremo de orden inferior del slot. La primera variable declarada de un grupo de empaquetado ocupa los bytes menos significativos, y las variables posteriores llenan hacia arriba. Un uint128 seguido de otro uint128, por lo tanto, devuelve una sola palabra de 32 bytes cuyos 16 bytes inferiores son la primera variable y los 16 bytes superiores son la segunda.
Debido a que los desplazamientos dependen del orden de declaración, reordenar las declaraciones cambia todos los desplazamientos del grupo de empaquetado. Una secuencia uint8, uint8, uint128 se empaqueta en un solo slot con el primer uint8 en el byte más bajo, el segundo en el siguiente byte y el uint128 en los 16 bytes superiores. Si mueves el uint128 al frente, los dos valores uint8 se desplazan a bytes más altos y tu máscara anterior devuelve silenciosamente el número equivocado.
La consecuencia práctica es que cualquier máscara o desplazamiento codificado de forma fija está vinculado a un orden de declaración y una versión del compilador específicos. Trátalo como un valor derivado que debe recalcularse cada vez que cambie el código fuente del contrato, no como una constante estable.
- Primera variable declarada de un grupo de empaquetado = bytes menos significativos.
- Las variables posteriores llenan hacia el extremo más significativo del mismo slot.
- Reordenar las declaraciones invalida todos los desplazamientos calculados previamente.
Aritmética de slots para mappings y arrays dinámicos
La referencia de layout de almacenamiento de Solidity define la derivación. Los mappings y los arrays dinámicos no almacenan sus elementos en slots contados. Un mapping ocupa un slot que solo contiene una semilla, y el slot de un elemento es keccak256(abi.encode(key, slot)). Un array dinámico ocupa un slot que contiene su longitud, y el slot de un elemento es keccak256(abi.encode(slot)) + index. Estos slots son derivados, no contados, por lo que no puedes encontrarlos sumando uno al slot de declaración.
La derivación es determinista y reproducible en JavaScript con una implementación de keccak256. El detalle clave es que tanto la clave del mapping como el número de slot se codifican como valores de 32 bytes antes del hash, por lo que una clave uint256 y un slot uint256 se concatenan como dos palabras de 32 bytes. Para arrays dinámicos, el slot base se hashea solo y el índice se suma al entero grande resultante.
Por esto, una lectura de un mapping en el slot de declaración devuelve cero o una semilla en lugar del valor esperado. El slot de declaración es metadato; el elemento vive en una ubicación derivada por hash que depende de la clave.
- Slot de elemento de mapping = keccak256(abi.encode(key, slot)).
- Slot de elemento de array dinámico = keccak256(abi.encode(slot)) + index.
- Ambos son derivados, por lo que no se pueden alcanzar con una simple suma.
Decodificación del valor de retorno de 32 bytes según el tipo de Solidity
eth_getStorageAt devuelve una cadena hexadecimal de 32 bytes independientemente del tipo de la variable, por lo que la decodificación depende del tipo de Solidity que esperes. Un bool es 0x00...01 para true y 0x00...00 para false. Una dirección ocupa los 20 bytes inferiores, por lo que el valor significativo son los últimos 40 caracteres hexadecimales. Un entero con signo necesita interpretación en complemento a dos, lo que significa que un valor con el bit alto establecido representa un número negativo.
Las cadenas y bytes de más de 31 bytes usan un esquema de longitud más datos: el slot contiene un campo de longitud, y los datos reales viven en slots separados derivados de keccak256 del slot base. Una sola llamada a eth_getStorageAt no puede recuperar la cadena completa; debes leer la longitud, luego leer los slots de datos y después ensamblar los bytes.
Para valores de 31 bytes o menos, Solidity almacena los datos en el propio slot con el bit bajo del último byte usado como bandera, por lo que las cadenas cortas son legibles en una sola llamada pero aún requieren una decodificación cuidadosa. El enfoque más seguro es decodificar contra un contrato conocido y confirmar con una función view pública.
- bool: 0x00...01 o 0x00...00.
- address: 20 bytes inferiores, últimos 40 caracteres hexadecimales.
- entero con signo: interpretación en complemento a dos.
- cadena/bytes largos: longitud en un slot, datos en slots derivados de keccak256.
Ejemplo ejecutable en Node.js: leer un slot y decodificar un par empaquetado
El siguiente ejemplo lee un slot, convierte el resultado a BigInt y decodifica un par empaquetado uint128/uint128 con una máscara y un desplazamiento explícitos. Imprime tanto el hexadecimal crudo como los valores decodificados para que puedas verificar la aritmética contra un contrato conocido. Reemplaza la URL RPC y la dirección del contrato con tus propios valores.
La máscara usa (1n << 128n) - 1n para aislar los 128 bits inferiores, y el desplazamiento usa >> 128n para extraer los 128 bits superiores. Esto refleja la regla de empaquetado documentada donde la primera variable declarada ocupa los bytes de orden inferior.
const { ethers } = require('ethers');
async function main() {
const provider = new ethers.JsonRpcProvider('https://your-rpc-endpoint');
const address = '0xYourContractAddress';
const slot = 0;
const raw = await provider.send('eth_getStorageAt', [address, '0x' + slot.toString(16), 'latest']);
console.log('raw:', raw);
const word = BigInt(raw);
const mask128 = (1n << 128n) - 1n;
const low = word & mask128;
const high = word >> 128n;
console.log('low (first declared):', low.toString());
console.log('high (second declared):', high.toString());
}
main().catch(console.error);Ejemplo ejecutable en Node.js: derivar el slot de un elemento de mapping
Este ejemplo deriva el slot de un elemento de mapping en JavaScript con una implementación de keccak256 y lo lee. Usa ethers para codificar la clave y el slot como valores de 32 bytes, los concatena y hashea el resultado. El mismo patrón se aplica a arrays dinámicos, donde se hashea el slot base y se suma el índice.
La plantilla es intencionalmente explícita para que puedas adaptarla a cualquier tipo de clave. Para un mapping en el slot 3 con una clave de tipo address, la clave codificada se rellena a la izquierda hasta 32 bytes y el slot también son 32 bytes, luego se aplica keccak256 a la concatenación.
const { ethers } = require('ethers');
async function main() {
const provider = new ethers.JsonRpcProvider('https://your-rpc-endpoint');
const address = '0xYourContractAddress';
const mappingSlot = 3;
const key = '0xYourKeyAddress';
const encoded = ethers.concat([
ethers.zeroPadValue(key, 32),
ethers.zeroPadValue('0x' + mappingSlot.toString(16), 32)
]);
const elementSlot = ethers.keccak256(encoded);
console.log('element slot:', elementSlot);
const raw = await provider.send('eth_getStorageAt', [address, elementSlot, 'latest']);
console.log('raw value:', raw);
console.log('decoded:', BigInt(raw).toString());
}
main().catch(console.error);Verificar una decodificación contra una función view pública
La forma más rápida de detectar un error de layout es leer el mismo valor a través de una función view pública con eth_call y comparar. Si el contrato expone un getter, llámalo y compara el valor devuelto con tu slot decodificado. Una discrepancia significa que tu aritmética de slots, máscara o desplazamiento es incorrecta, o que el contrato usa un proxy con un layout de almacenamiento diferente.
Esto convierte una suposición sobre el layout en un hecho verificado. También detecta diferencias de versión del compilador y efectos de optimización que cambian el empaquetado. La página simulación de anulación de estado con eth_call cubre técnicas de simulación relacionadas, y la página eth_getProof y pruebas de cuenta/almacenamiento explica cómo verificar el almacenamiento sin confiar en el endpoint.
Para una comprobación reproducible, registra el valor crudo del slot, tu valor decodificado y el resultado de la función view en una tabla. Si no coinciden, inspecciona el orden de declaración y la versión del compilador antes de cambiar la máscara.
- Llama al getter público con eth_call y compáralo con tu decodificación.
- Una discrepancia indica slot, máscara, desplazamiento o layout de proxy incorrectos.
- Registra los valores crudo, decodificado y esperado para cada comprobación.
El riesgo del layout de almacenamiento en proxies
En un patrón proxy, el orden de declaración de la implementación no coincide con el almacenamiento del proxy. Los slots legibles están determinados por el layout del proxy, que normalmente reserva los primeros slots para el admin, la dirección de implementación y otro estado del proxy. La aritmética de slots debe tomarse del layout de almacenamiento desplegado en lugar del código fuente de la implementación más reciente.
Este es un modo de fallo común: un desarrollador lee el código fuente de la implementación, calcula el slot 0 y obtiene la dirección del admin del proxy en lugar de la variable deseada. La solución es obtener el layout de almacenamiento a partir de la salida del compilador del proxy o de una herramienta de layout de almacenamiento verificada, y tener en cuenta cualquier hueco o slot reservado.
Si te integras con un proxy, trata el layout de almacenamiento como parte de la interfaz del contrato desplegado. Las páginas guía de endpoints RPC (RPC Assistant) y servicio de API describen cómo conectarse a las redes donde están desplegados estos contratos.
- El layout del proxy, no el código fuente de la implementación, determina los slots legibles.
- Los slots reservados para admin e implementación desplazan todas las variables.
- Usa la salida verificada del layout de almacenamiento para el proxy desplegado.
Compromiso frente a eth_getProof y las pruebas de almacenamiento
eth_getProof devuelve el mismo valor con una prueba de Merkle que se puede verificar sin confiar en el endpoint, a costa de una respuesta más pesada y lógica de verificación adicional. eth_getStorageAt es más ligero y sencillo, pero confía en que el endpoint devuelva el valor correcto para el slot solicitado.
Para paneles de solo lectura y depuración, eth_getStorageAt suele ser suficiente. Para aplicaciones que no deben confiar en un único proveedor, eth_getProof es la opción más sólida. El artículo eth_getProof y pruebas de cuenta/almacenamiento cubre el flujo de verificación en detalle.
Un patrón práctico es usar eth_getStorageAt durante el desarrollo y cambiar a eth_getProof para rutas de producción donde la corrección deba ser verificable de forma independiente. La página precios de RPC puede ayudarte a estimar la diferencia de coste entre ambos métodos.
- eth_getStorageAt: ligero, simple, confía en el endpoint.
- eth_getProof: más pesado, verificable sin confiar en el endpoint.
- Elige según si se requiere verificación independiente.
Solución de problemas comunes en la lectura de slots
Cuando una lectura de slot devuelve un valor inesperado, revisa primero el orden de declaración. Si la variable comparte slot con otras, aplica la máscara y el desplazamiento correctos. Si la variable es un elemento de mapping o de array dinámico, verifica que hayas derivado el slot con keccak256 en lugar de contarlo. Si el contrato es un proxy, confirma que estás usando el layout de almacenamiento del proxy.
Otro problema frecuente es la selección de la etiqueta de bloque. Leer en 'latest' devuelve el estado actual, mientras que leer en un bloque histórico devuelve el estado en ese bloque. Si un valor cambió recientemente, una lectura histórica puede devolver el valor antiguo. Confirma también que la posición sea una cantidad de 32 bytes; una cadena hexadecimal corta puede ser rechazada o malinterpretada por algunos proveedores.
Por último, revisa la versión del compilador y la configuración de optimización. El comportamiento de empaquetado está documentado, pero puede cambiar entre versiones del compilador, por lo que una decodificación que funcionó para una compilación puede fallar para otra. Vuelve a derivar el layout a partir de la salida del compilador actual.
- Verifica el orden de declaración y el grupo de empaquetado.
- Confirma que los slots de mappings/arrays se derivan con keccak256.
- Revisa el layout del proxy y la etiqueta de bloque.
- Vuelve a derivar el layout tras cambios de compilador u optimización.
Medir el comportamiento de tu endpoint con una tabla de resultados
El comportamiento de los proveedores para eth_getStorageAt está documentado, pero varía según el proveedor en áreas como la disponibilidad de estado histórico, los límites de tasa y el formato de errores. Para medir tu propio endpoint, ejecuta un pequeño conjunto de lecturas contra un contrato conocido y registra los resultados en una tabla. Esto convierte las afirmaciones del proveedor en hechos observados para tu integración.
Usa un contrato con un layout de almacenamiento conocido, lee un slot por orden de declaración, un slot empaquetado y un slot de elemento de mapping, y compara cada uno con una función view pública. Registra el hexadecimal crudo, el valor decodificado, el valor esperado y el tiempo de ida y vuelta. Repite en 'latest' y en un bloque histórico para observar la disponibilidad de estado.
La tabla siguiente es una plantilla para rellenar con tus propias mediciones. No trates los números de ningún proveedor concreto como universales; el objetivo es establecer una línea base para tu propio entorno.
- Columnas: tipo de slot, hexadecimal crudo, valor decodificado, valor esperado, etiqueta de bloque, tiempo de ida y vuelta.
- Filas: slot de declaración, slot empaquetado, slot de elemento de mapping, lectura histórica.
- Compara los valores decodificados con los resultados del getter vía eth_call para cada fila.
Limitaciones y siguientes pasos para integraciones basadas en slots
Los números de slot son un detalle de implementación del compilador que puede cambiar entre versiones del compilador y con las optimizaciones. Cualquier slot codificado de forma fija es una carga de mantenimiento que debe recalcularse cada vez que se recompile el contrato. Trata la aritmética de slots como un artefacto de tiempo de compilación, no como una constante en tiempo de ejecución.
Para integraciones en producción, prefiere funciones view públicas cuando estén disponibles, usa eth_getStorageAt para diagnósticos y para contratos sin getters, y usa eth_getProof cuando se requiera verificación independiente. El centro de aprendizaje de OnFinality recopila guías relacionadas, y los artículos eth_getProof y pruebas de cuenta/almacenamiento, simulación de anulación de estado con eth_call, gestión de nonces EVM con eth_getTransactionCount y filtrado de temas de eventos de Ethereum con eth_getLogs cubren patrones de lectura adyacentes.
Para empezar, conéctate a un endpoint de Ethereum desde la página de redes, ejecuta los ejemplos anteriores contra un contrato que controles y registra tus resultados en la tabla de medición. Las páginas guía de endpoints RPC (RPC Assistant) y servicio de API explican cómo aprovisionar y gestionar endpoints para este flujo de trabajo.
- Vuelve a derivar los slots después de cada recompilación o actualización del compilador.
- Prefiere funciones view cuando estén disponibles; usa lecturas de almacenamiento para diagnósticos.
- Usa eth_getProof cuando se requiera verificación independiente.