Por Fran Palacios
En este artículo, vamos a profundizar un poco en el almacenamiento persistente en blockchains EVM1 compatibles. En otros lenguajes, no es necesario conocer qué sucede a bajo nivel para poder hacer código eficiente, en el caso de Solidity (que no llega a ser un lenguaje de alto nivel) sí que es importante, mas no imprescindible conocer esto, porque las operaciones cuestan gas.
El gas, es la unidad que mide el cantidad de esfuerzo computacional requerida para ejecutar operaciones en la blockchain. En Ethereum, tenemos un modelo transaccional, y cada una de ellas, requiere computo para ejecutarse y validarse, por tanto hay que pagar una comisión (gas). En este fragmento de tabla, podemos ver el costo asociado a cada operación (opcode); vemos que los accesos a Memory (MLOAD & MSTORE) son muchísimo más baratos que los accesos al Storage (SLOAD & STORE).
Tabla completa: https://github.com/crytic/evm-opcodes
A priori hay 2 tipos de almacenamiento en Solidity, el volátil (Memory) que podemos verlo como si fuera la memoria RAM, y el persistente (Storage), que podemos mirarlo cómo un disco duro.
|
|
|
|
Todos los SmartContracts que corren en la EVM, tienen un Storage permanente asociado, cada uno forma parte del estado global de la blockchain. Este Storage, se puede ver como una lista de pares clave-valor
ordenadas por su declaración en el SmartContract, donde cada elemento de la lista, es de 32 bytes y cuyo tamaño total es de 2^256. Es suficientemente grande para almacenar cualquier cosa, pero inicialmente está todo a 0 y por eso no cuesta nada. En Ethereum, se paga por todo lo que no es 0 y por ampliaciones de memoria.
Así es, en este lenguaje es relevante el orden en que se declaran las variables, no se suelen agrupar por dominio u otros criterios, sino por tamaño y tipo para facilitarle el trabajo a la EVM.
Veamos este contrato:
|
|
Para inspeccionar el estado del Storage, podemos utilizar la function getStorageAt que nos da el proveedor (en este caso usaremos ethers), le pasamos la dirección de un contrato y el slot de memoria del dato que queremos obtener.
|
|
Esto nos imprime en consola las 5 primeras posiciones de memoria en el Storage.
|
|
Y tal y como esperábamos, el 5 se guardó en la posición 0 y el True en la posición 1.
Si hacemos una prueba con tipos de dato más pequeños, podemos ver como comparten un slot de memoria. Por ejemplo si reemplazamos uint256 favNumber
por uint8 favNumber
produce:
|
|
Vemos que se tiene que tener en cuenta el tamaño y tipo de las variables. En la documentación de Solidity vienen definidas las reglas para declarar variables de manera eficiente.
Ahora veamos ejemplos usando arrays (fijos y dinámicos) y mappings2.
Los arrays de tamaño fijo se almacenan en posiciones consecutivas. Podemos comprobarlo con el mismo método de antes:
|
|
Vemos que los slots que contienen 0x00..A, 0x00...14 y 0x00...1e
corresponden con los valores en hexadecimal de 10,20 y 30.
|
|
¿Y qué pasa con los arrays dinámicos? ¿Cómo sabe la EVM cuántos slots reservar entre variables?
Pues no lo sabe 😅, y para solucionarlo utiliza una función de hash keccak256
, para encontrar una posición de memoria y no sobreescribir otros slots. Cambiemos un poco el código para meter un array dinámico. Reemplazamos uint256[3] myArray
con uint256[] myArray
. El resto de código queda igual insertando los mismos 3 elementos.
|
|
Vemos que han desaparecido, pero en la posición 2 tenemos guardado el número 3, este número es la longitud del array y se va actualizando a medida que se insertan elementos, pero lo que no cambia es el slot donde está, sigue siendo el 2. Para calcular la posición a partir de la cual se guardan los datos del array, se utiliza la función keccak256(slot-donde-esta-la-longitud-del-array)
. Ethers nos provee dicha función:
Sabemos que está en la posición 2 (0x2 en hexadecimal), con hexZeroPad
lo que hacemos es rellenar con 0 a la izquierda hasta llegar a 32 bytes (64 ceros).
|
|
Ahora, si llamamos a getStorageAt
con ese slot, obtendremos el primer dato del array:
|
|
Si lo pasamos a decimal A = 10, ¡nuestro primer elemento! ahora los siguientes elementos están en orden secuencial, es decir, si sumamos 1 a la posición del primero, nos encontraremos con el segundo.
|
|
Que si lo pasamos a decimal nos da 20. Y el tercer dato estará en la siguiente:
|
|
Por último, veremos un ejemplo con los mappings.
|
|
En este output, vemos que el slot 2 está completamente vacío, a diferencia del array dinámico, aquí no se guarda la longitud del mapping porque no es iterable, y sus claves no tienen porque ser consecutivas.
|
|
Sin embargo, para calcular la posición de memoria donde guardar los datos, hace algo similar que los arrays. En este caso, la fórmula a aplicar es esta: keccak256(h(k) . p)
donde h
es una función basada en el tipo de la clave, k
es la clave del diccionario, p
es el slot de memoria del mapping y .
es la concatenación de ambas.
|
|
Como curiosidad de esta función, (ya hemos visto para lo que sirve), nos permite acceder al contenido de un slot de memoria del storage. Si le das una vuelta, te das cuenta que puedes verlo todo, siempre y cuando tengas acceso al contrato, podrás ver en qué slot va guardado algo.
Como se ve en el último ejemplo, hay variables públicas (por defecto) y privadas (las que llevan private
por delante), así que usando esta función, puedes verlas todas si tienes acceso al código del contrato y simplemente cuentas en qué slot está definida la variable.
Ethereum Virtual Machine. Es una entidad sustentada por los nodos que conforman la red. Podemos verla como una máquina de estado distribuida. No comparte recursos en cuanto a potencia de cómputo con otros nodos, pero sí comparte el estado global. Es capaz de ejectuar código. ↩︎
Es un diccionario clave-valor, no es iterable y su tamaño es dinámico. Su sintaxis es (<tipo-clave> ⇒ <tipo-valor>) nombre-de-variable. Los mappings pueden ser anidados. ↩︎
¿Quieres más? te invitamos a suscribirte a nuestro boletín para avisarte cada vez que recopilemos contenido de calidad que compartir.
Si disfrutas leyendo nuestro blog, ¿imaginas lo divertido que sería trabajar con nosotros? ¿te gustaría?
Pero espera 🖐 que tenemos un conflicto interno. A nosotros las newsletter nos parecen 💩👎👹 Por eso hemos creado la LEAN LISTA, la primera lista zen, disfrutona y que suena a rock y reggaeton del sector de la programación. Todos hemos recibido newsletters por encima de nuestras posibilidades 😅 por eso este es el compromiso de la Lean Lista