Hasta este punto, ninguno de nuestros tutoriales ha utilizado “variables de almacenamiento” ni ha almacenado nada de forma permanente.
En Solidity y Ethereum, un patrón de diseño más exótico para almacenar datos es SSTORE2 o SSTORE3, donde los datos se almacenan en el bytecode de otro contrato inteligente.
En Solana, este no es un patrón de diseño exótico, ¡es la norma!
Recuerda que podemos actualizar el bytecode de un programa de Solana (si somos el implementador original) a voluntad, a menos que el programa esté marcado como inmutable.
Solana utiliza el mismo mecanismo para el almacenamiento de datos.
Los slots de almacenamiento en Ethereum son, en efecto, un almacenamiento clave-valor masivo:
{
key: [smart_contract_address, storage slot]
value: 32_byte_slot // (for example: 0x00)
}
El modelo de Solana es similar: es un almacenamiento clave-valor masivo donde la “clave” es una dirección codificada en base 58 y el valor es un bloque de datos que puede tener un tamaño de hasta 10MB (u opcionalmente no contener nada). Se puede visualizar de la siguiente manera:
{
// key is a base58 encoded 32 byte sequence
key: ETnqC8mvPRyUVXyXoph22EQ1GS5sTs1zndkn5eGMYWfs
value: {
data: 020000006ad1897139ac2bdb67a3c66a...
// other fields are omitted
}
}
En Ethereum, el bytecode de un contrato inteligente y las variables de almacenamiento de un contrato inteligente se almacenan por separado, es decir, se indexan de manera diferente y deben cargarse utilizando diferentes APIs.
El siguiente diagrama muestra cómo Ethereum mantiene el estado. Cada cuenta es una hoja en un árbol de Merkle. Ten en cuenta que las “variables de almacenamiento” se almacenan “dentro” de la cuenta del contrato inteligente (Account 1).

En Solana, todo es una cuenta que potencialmente puede contener datos. A veces nos referimos a una cuenta como una “cuenta de programa” o a otra cuenta como una “cuenta de almacenamiento”, pero la única diferencia es si la bandera ejecutable está configurada en true y cómo pretendemos utilizar el campo de datos de la cuenta.
A continuación, podemos ver que el almacenamiento de Solana es un gigantesco almacenamiento clave-valor que asocia direcciones de Solana con una cuenta:

Imagina si Ethereum no tuviera variables de almacenamiento y los contratos inteligentes fueran mutables por defecto. Para almacenar datos, tendrías que crear otros “contratos inteligentes” y mantener los datos en su bytecode, para luego modificarlos cuando fuera necesario. Este es un modelo mental de Solana.
Otro modelo mental para esto es cómo en Unix todo es un archivo, solo que algunos archivos son ejecutables. Las cuentas de Solana pueden considerarse como archivos. Contienen información, pero también tienen metadatos que indican quién es el propietario del archivo, si es ejecutable, y así sucesivamente.
En Ethereum, las variables de almacenamiento están acopladas directamente al contrato inteligente. A menos que un contrato inteligente otorgue acceso de escritura o lectura a través de variables públicas, delegatecall o algún método setter, por defecto, una variable de almacenamiento solo puede ser leída o escrita por un único contrato (aunque cualquiera puede leer las variables de almacenamiento fuera de la cadena). En Solana, todas las “variables de almacenamiento” pueden ser leídas por cualquier programa, pero solo su programa propietario puede escribir en ellas.
La forma en que el almacenamiento está “vinculado a” un programa es a través del campo owner.
En la imagen de abajo, vemos que la cuenta B es propiedad de la cuenta de programa A. Sabemos que A es una cuenta de programa porque “executable” está configurado en true. Esto indica que el campo de datos de B estará almacenando datos para A:

Los programas de Solana necesitan ser inicializados antes de poder ser usados
En Ethereum, podemos escribir directamente en una variable de almacenamiento que no hemos utilizado antes. Sin embargo, los programas de Solana necesitan una transacción de inicialización explícita. Es decir, tenemos que crear la cuenta antes de poder escribir datos en ella.
Es posible inicializar y escribir en una cuenta de Solana en una sola transacción — sin embargo, esto introduce problemas de seguridad que complicarán la discusión si los abordamos ahora. Por ahora, es suficiente decir que las cuentas de Solana deben ser inicializadas antes de poder ser utilizadas.
Un ejemplo básico de almacenamiento
Traduzcamos el siguiente código de Solidity a Solana:
contract BasicStorage {
Struct MyStorage {
uint64 x;
}
MyStorage public myStorage;
function set(uint64 _x) external {
myStorage.x = _x;
}
}
Puede parecer extraño que hayamos envuelto una sola variable en un struct.
Pero en los programas de Solana, particularmente en Anchor, todo el almacenamiento, o más bien los datos de la cuenta, se trata como un struct. La razón se debe a la flexibilidad de los datos de la cuenta. Dado que las cuentas son bloques de datos que pueden ser bastante grandes (hasta 10MB), necesitamos cierta “estructura” para interpretar los datos; de lo contrario, es solo una secuencia de bytes sin sentido.
Entre bambalinas, Anchor deserializa y serializa los datos de la cuenta en structs cuando intentamos leer o escribir los datos.
Como se mencionó anteriormente, necesitamos inicializar la cuenta de Solana antes de poder utilizarla, por lo que antes de implementar la función set(), necesitamos escribir la función initialize().
Código boilerplate para la inicialización de cuentas
Creemos un nuevo proyecto de Anchor llamado basic_storage.
A continuación hemos escrito el código mínimo para inicializar un struct MyStorage, que solo contiene un número, x. (Ver el struct MyStorage en la parte inferior del código):
use anchor_lang::prelude::*;
use std::mem::size_of;
declare_id!("GLKUcCtHx6nkuDLTz5TNFrR4tt4wDNuk24Aid2GrDLC6");
#[program]
pub mod basic_storage {
use super::*;
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
Ok(())
}
}
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(init,
payer = signer,
space=size_of::<MyStorage>() + 8,
seeds = [],
bump)]
pub my_storage: Account<'info, MyStorage>,
#[account(mut)]
pub signer: Signer<'info>,
pub system_program: Program<'info, System>,
}
#[account]
pub struct MyStorage {
x: u64,
}
1) La función initialize
Nota que no hay código en la función initialize() — de hecho, todo lo que hace es retornar Ok(()):
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
Ok(())
}
No es obligatorio que las funciones para inicializar cuentas estén vacías, podríamos tener una lógica personalizada. Pero para nuestro ejemplo, está vacía. Tampoco es obligatorio que las funciones que hacen initialize en las cuentas se llamen initialize, pero es un nombre útil.
2) El struct Initialize
El struct Initialize contiene referencias a los recursos necesarios para inicializar una cuenta:
my_storage: un struct de tipoMyStorageque estamos inicializando.signer: la billetera que está pagando por el “gas” para el almacenamiento del struct. (Los costos de gas para el almacenamiento se discutirán más adelante).system_program: lo discutiremos más adelante en este tutorial.

La palabra clave 'info es un Rust lifetime. Ese es un tema extenso y es mejor tratarlo como boilerplate por ahora.
Nos centraremos en la macro por encima de my_storage, ya que aquí es donde ocurre la acción para la inicialización.
3) El campo my_storage en el struct Initialize
La macro de atributo por encima del campo my_storage (flecha morada) es cómo Anchor sabe que esta transacción está destinada a inicializar esta cuenta (recuerda, una attribute-like macro comienza con # y aumenta el struct con funcionalidad adicional):

La palabra clave importante aquí es init.
Cuando hacemos init a una cuenta, debemos proporcionar información adicional:
payer(caja azul): quién está pagando el SOL para asignar el almacenamiento. El signer se especifica comomutporque el saldo de su cuenta cambiará, es decir, se deducirá algo de SOL de su cuenta. Por lo tanto, anotamos su cuenta como “mutable.”space(caja naranja): esto indica cuánto espacio ocupará la cuenta. En lugar de averiguarlo nosotros mismos, podemos usar la utilidadstd::mem::size_ofy utilizar el struct que estamos intentando almacenar:MyStorage(caja verde), como argumento. El+ 8(caja rosa) lo discutiremos en el siguiente punto.seedsybump(caja roja): Un programa puede poseer múltiples cuentas, este “discrimina” entre las cuentas con la “semilla” que se utiliza para calcular un “discriminador”. El “discriminador” ocupa 8 bytes, por lo que necesitamos asignar los 8 bytes adicionales además del espacio que ocupa nuestro struct. El bump puede ser tratado como boilerplate por ahora.
Esto puede parecer mucha información para asimilar, no te preocupes. Inicializar una cuenta puede tratarse en gran medida como boilerplate por ahora.
4) ¿Qué es el system program?
El system program es un programa integrado en el entorno de ejecución de Solana (un poco como un Ethereum precompile) que transfiere SOL de una cuenta a otra. Retomaremos esto en un tutorial posterior sobre cómo transferir SOL. Por ahora, necesitamos transferir SOL desde el signer, que es quien está pagando por el almacenamiento de MyStruct, por lo que el system program siempre es parte de las transacciones de inicialización.
5) El struct MyStorage
Recuerda el campo data dentro de la cuenta de Solana:

Bajo el capó, esto es una secuencia de bytes. El struct en el ejemplo de arriba:
#[account]
pub struct MyStorage {
x: u64,
}
se serializa en una secuencia de bytes y se almacena en el campo data cuando se escribe en él. Durante la escritura, el campo data se deserializa de acuerdo a ese struct.
En nuestro ejemplo, solo estamos usando una variable en el struct, aunque podríamos agregar más, o variables de otro tipo, si quisiéramos.
El entorno de ejecución de Solana no nos obliga a usar structs para almacenar datos. Desde la perspectiva de Solana, la cuenta solo contiene un bloque de datos. Sin embargo, Rust tiene muchas bibliotecas convenientes para convertir structs en bloques de datos y viceversa, por lo que los structs son la convención. Anchor está aprovechando estas bibliotecas tras bambalinas.
No es obligatorio utilizar structs para usar las cuentas de Solana. Es posible escribir secuencias de bytes directamente, pero esta no es una manera conveniente de almacenar datos.
La macro #[account] implementa toda la magia de forma transparente.
6) Prueba unitaria de inicialización
El siguiente código en Typescript ejecutará el código en Rust de arriba.
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { BasicStorage } from "../target/types/basic_storage";
describe("basic_storage", () => {
anchor.setProvider(anchor.AnchorProvider.env());
const program = anchor.workspace.BasicStorage as Program<BasicStorage>;
it("Is initialized!", async () => {
const seeds = []
const [myStorage, _bump] = anchor.web3.PublicKey.findProgramAddressSync(seeds, program.programId);
console.log("the storage account address is", myStorage.toBase58());
await program.methods.initialize().accounts({ myStorage: myStorage }).rpc();
});
});
Aquí está la salida de la prueba unitaria:

Aprenderemos más sobre esto en un tutorial posterior, pero Solana requiere que especifiquemos de antemano las cuentas con las que interactuará una transacción. Dado que estamos interactuando con la cuenta que almacena MyStruct, necesitamos calcular su “dirección” por adelantado y pasarla a la función initialize(). Esto se hace con el siguiente código en Typescript:
seeds = []
const [myStorage, _bump] =
anchor.web3.PublicKey.findProgramAddressSync(seeds, program.programId);
Nota que seeds es un array vacío, al igual que lo es en el programa de Anchor.
Predecir la dirección de la cuenta en Solana es como create2 en Ethereum
En Ethereum, la dirección de un contrato creado usando create2 depende de:
- la dirección del contrato implementador
- un salt
- y el bytecode del contrato creado
Predecir la dirección de cuentas inicializadas en Solana es muy similar, excepto que ignora el “bytecode”. Específicamente, depende de:
- el programa que posee la cuenta de almacenamiento,
basic_storage(lo cual es similar a la dirección del contrato implementador) - y los
seeds(lo cual es similar al “salt” de create2)
En todos los ejemplos de este tutorial, seeds es un array vacío, pero exploraremos arrays no vacíos en un tutorial posterior.
No olvides convertir my_storage a myStorage
Anchor convierte silenciosamente el snake case de Rust al camel case de Typescript. Cuando proporcionamos .accounts({myStorage: myStorage}) en Typescript a la función initialize, está “completando” la clave my_storage en el struct Initialize en Rust (círculo verde a continuación). El system_program y el Signer son completados de forma transparente por Anchor:

Las cuentas no pueden ser inicializadas dos veces
Si pudiéramos reinicializar una cuenta, ¡eso sería muy problemático ya que un usuario podría borrar datos del sistema! Afortunadamente, Anchor se defiende de esto en segundo plano.
Si ejecutas la prueba por segunda vez (sin reiniciar el validador local), obtendrás el error en la captura de pantalla de abajo.
Alternativamente, puedes ejecutar la siguiente prueba si no estás usando el validador local:
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { BasicStorage } from "../target/types/basic_storage";
describe("basic_storage", () => {
anchor.setProvider(anchor.AnchorProvider.env());
const program = anchor.workspace.BasicStorage as Program<BasicStorage>;
it("Is initialized!", async () => {
const seeds = []
const [myStorage, _bump] = anchor.web3.PublicKey.findProgramAddressSync(seeds, program.programId);
// ********************************************
// **** NOTE THAT WE CALL INITIALIZE TWICE ****
// ********************************************
await program.methods.initialize().accounts({myStorage: myStorage}).rpc();
await program.methods.initialize().accounts({myStorage: myStorage}).rpc();
});
});
Cuando ejecutamos la prueba, la prueba falla porque la segunda llamada a initialize arroja un error. La salida esperada es la siguiente:

No olvides reiniciar el validador si ejecutas la prueba varias veces
Debido a que el solana-test-validator seguirá recordando la cuenta de la primera prueba unitaria, querrás reiniciar el validador entre pruebas usando solana-test-validator --reset. De lo contrario, obtendrás el error anterior.
Resumen de la inicialización de cuentas
La necesidad de inicializar una cuenta probablemente parecerá antinatural para la mayoría de los desarrolladores de EVM.
No te preocupes, verás esta secuencia de código una y otra vez, y se volverá natural después de un tiempo.
Solo hemos examinado la inicialización del almacenamiento en este tutorial; en los próximos estudiaremos cómo leer, escribir y eliminar almacenamiento. Habrá muchas oportunidades para comprender intuitivamente lo que hace todo el código que vimos hoy.
Ejercicio: modifica MyStorage para que contenga x e y como si fuera una coordenada cartesiana. Esto significa agregar y al struct MyStorage y cambiarlos de u64 a i64. No necesitarás modificar otras partes del código porque size_of recalculará el tamaño por ti. Asegúrate de reiniciar el validador para que la cuenta de almacenamiento original se borre y no se te bloquee la inicialización de la cuenta de nuevo.
Aprende más con RareSkills
Consulta nuestro curso de Solana para aprender más.
Publicado originalmente el 24 de febrero de 2024