En Solidity, leer el almacenamiento de otro contrato requiere llamar a una función view o que la variable de almacenamiento sea pública. En Solana, un cliente off-chain puede leer una cuenta de almacenamiento directamente. Este tutorial muestra cómo un programa de Solana on-chain puede leer los datos en una cuenta de la que no es propietario.
Configuraremos dos programas: data_holder y data_reader. data_holder inicializará y poseerá un PDA con datos que data_reader leerá.
Configurar el programa data_holder que almacena los datos: Shell 1
El siguiente código es un programa básico de Solana que inicializa la cuenta Storage con el campo x de tipo u64 y almacena el valor 9 en él durante la inicialización:
Código en TypeScript:
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { DataHolder } from "../target/types/data_holder";
describe("data-holder", () => {
anchor.setProvider(anchor.AnchorProvider.env());
const program = anchor.workspace.DataHolder as Program<DataHolder>;
it("Is initialized!", async () => {
const seeds = [];
const [storage, _bump] = anchor.web3.PublicKey.findProgramAddressSync(
seeds,
program.programId
);
await program.methods
.initialize()
.accounts({ storage: storage })
.rpc();
let storageStruct = await program.account.storage.fetch(
storage
);
console.log("The value of x is: ",storageStruct.x.toString());
console.log("Storage account address: ", storage.toBase58());
});
});
La prueba imprimirá la dirección del PDA, nos referiremos a esta dirección en breve:

Lector (Reader)
Para que el data_reader lea otra cuenta, la clave pública de esa cuenta debe pasarse como parte de la transacción a través del struct Context. Esto no es diferente a pasar cualquier otro tipo de cuenta.
Los datos en las cuentas se almacenan como bytes serializados. Para deserializar la cuenta, el programa data_reader necesita una definición en Rust del struct que está leyendo. Necesitaremos la siguiente definición de cuenta disponible para data_reader, que es idéntica al struct Storage en data_holder:
#[account]
pub struct Storage {
x: u64,
}
Este struct es perfectamente idéntico al de data_reader — incluso el nombre debe ser el mismo (más adelante entraremos en detalles de por qué). El código para leer la cuenta está en las siguientes dos líneas:
let mut data_slice: &[u8] = &data_account.data.borrow();
let data_struct: Storage =
AccountDeserialize::try_deserialize(
&mut data_slice,
)?;
El data_slice representa los bytes crudos (raw) de los datos en la cuenta. Si ejecutas solana account <pda address> (usando la dirección del PDA generada cuando desplegamos data_holder) puedes ver los datos allí, incluido el número 9 que almacenamos en el recuadro rojo:

Los primeros 8 bytes en el recuadro amarillo son el discriminador de cuenta (account discriminator), que describiremos más adelante.
La deserialización ocurre en este paso:
let data_struct: Storage =
AccountDeserialize::try_deserialize(
&mut data_slice,
)?;
Pasar el tipo Storage (el mismo struct que definimos arriba) aquí le indica a Solana cómo (intentar) deserializar los datos.
Ahora vamos a crear un proyecto de Anchor separado en una nueva carpeta con anchor new data_reader.
Aquí está el código completo en Rust:
use anchor_lang::prelude::*;
declare_id!("HjJ1Rqsth5uxA6HKNGy8VVRvwK4W7aFgmQsss7UxePBw");
#[program]
pub mod data_reader {
use super::*;
pub fn read_other_data(
ctx: Context<ReadOtherData>,
) -> Result<()> {
let data_account = &ctx.accounts.other_data;
if data_account.data_is_empty() {
return err!(MyError::NoData);
}
let mut data_slice: &[u8] = &data_account.data.borrow();
let data_struct: Storage =
AccountDeserialize::try_deserialize(
&mut data_slice,
)?;
msg!("The value of x is: {}", data_struct.x);
Ok(())
}
}
#[error_code]
pub enum MyError {
#[msg("No data")]
NoData,
}
#[derive(Accounts)]
pub struct ReadOtherData<'info> {
/// CHECK: We do not own this account so
// we must be very cautious with how we
// use the data
other_data: UncheckedAccount<'info>,
}
#[account]
pub struct Storage {
x: u64,
}
Y aquí está el código de prueba para ejecutarlo. Asegúrate de cambiar la dirección del PDA en el código a continuación:
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { DataReader } from "../target/types/data_reader";
describe("data-reader", () => {
anchor.setProvider(anchor.AnchorProvider.env());
const program = anchor.workspace
.DataReader as Program<DataReader>;
it("Is initialized!", async () => {
// CHANGE THIS TO THE ADDRESS OF THE PDA OF
// DATA ACCOUNT HOLDER
const otherStorageAddress ="HRGqGCLXxLryZav2SeKJKqBWYs8Ne7ppJxf3MLM3Y71E";
const pub_key_other_storage = new anchor.web3.PublicKey(
otherStorageAddress
);
const tx = await program.methods
.readOtherData()
.accounts({ otherData: pub_key_other_storage })
.rpc();
});
});
Para probar la lectura de los datos de otras cuentas:
- Ejecuta la prueba
data_holdercon elsolana-test-validatorejecutándose en segundo plano. - Copia y pega la clave pública impresa de la cuenta
Storage - Coloca esa clave pública en el
otherStorageAddressde la prueba paradata_reader - Ejecuta los registros (logs) de Solana en otra terminal (shell)
- Ejecuta la prueba de
data_readerpara leer los datos.
Lo siguiente debería ser visible en los registros de Solana:

¿Qué sucede si no le damos a los structs el mismo nombre?
Si cambias el struct Storage en data_reader a un nombre que no sea Storage, por ejemplo Storage2, e intentas leer la cuenta, ocurrirá el siguiente error:

El discriminador de cuenta calculado por Anchor son los primeros ocho bytes del sha256 del nombre del struct. El discriminador de cuenta no depende de las variables en el struct.
Cuando Anchor lee la cuenta, verifica los primeros ocho bytes (el discriminador de cuenta) para ver si coinciden con el discriminador de cuenta de la definición del struct que está utilizando localmente para deserializar los datos. Si no coinciden, Anchor no deserializará los datos.
Comprobar el discriminador de cuenta es una medida de seguridad contra el cliente pasando accidentalmente la cuenta equivocada o una cuenta cuyos datos no están en el formato que Anchor espera.
La deserialización no se revertirá si se analiza (parsea) un struct más grande
Anchor solo comprueba si el discriminador de cuenta coincide — no valida los campos dentro de la cuenta que se está leyendo.
Caso 1: Anchor no comprueba si el nombre del campo del struct coincide
Cambiemos el campo x en el struct Storage de data_reader a y, dejando el struct Storage en data_holder sin cambios:
// data_reader
#[account]
pub struct Storage {
y: u64,
}
También tendremos que cambiar la línea de registro (log) de la siguiente manera:
msg!("The value of y is: {}", data_struct.y);
Cuando volvemos a ejecutar la prueba, lee los datos correctamente:
Program log: Instruction: ReadOtherData
Program log: The value of y is: 9
Caso 2: Anchor no comprueba el tipo de datos
Ahora cambiemos el tipo de datos de y en Storage dentro de data_reader a u32 aunque el struct original sea u64.
// data_reader
#[account]
pub struct Storage {
y: u32,
}
Cuando ejecutamos la prueba, Anchor sigue analizando (parseando) con éxito los datos de la cuenta.
Program log: Instruction: ReadOtherData
Program log: The value of y using u32 is: 9
La razón por la que esto “tuvo éxito” se debe a cómo están dispuestos los datos:

El 9 en 7 está disponible en los primeros bytes — un u32 buscará datos en los primeros 4 bytes por lo que será capaz de “ver” el 9.
Por supuesto, si almacenáramos un valor en x que u32 no puede contener, como , entonces nuestro programa de lectura imprimirá un número incorrecto.
Ejercicio: Reinicia el validador y vuelve a desplegar data_holder con el valor . La forma de elevar a una potencia en Rust es let result = u64::pow(base, exponent). Por ejemplo, let result = u64::pow(2, 32); Observa qué valor registra data_reader en los logs.
Caso 3: Analizar más datos de los que hay
La cuenta de almacenamiento tiene un tamaño de 16 bytes. Contiene 8 bytes para el discriminador de cuenta y 8 bytes para la variable u64. Si intentamos leer más datos de los que hay, como por ejemplo definiendo un struct con valores que requieren más de 16 bytes de capacidad, la deserialización durante la lectura fallará:
#[account]
pub struct Storage {
y: u64,
z: u64,
}
El struct anterior requiere 16 bytes para almacenar y y z, pero se necesitan 8 bytes adicionales para contener el discriminador de cuenta, lo que hace que la cuenta tenga un tamaño de 24 bytes.

Resumen del análisis (parsing) de datos de cuentas de Anchor
Al leer los datos de una cuenta externa, Anchor comprobará si el discriminador de cuenta coincide y si hay suficientes datos en la cuenta para ser deserializados en el struct utilizado como tipo para try_deserialize:
let data_struct: Storage =
AccountDeserialize::try_deserialize(
&mut data_slice,
)?;
Anchor no comprueba los nombres de las variables ni su longitud.
A nivel interno (under the hood), Anchor no almacena ningún metadato sobre cómo interpretar los datos en la cuenta. Simplemente son los bytes de las variables almacenados consecutivamente (end-to-end).
No todas las cuentas de datos siguen la convención de Anchor
Solana no requiere el uso de discriminadores de cuenta. Los programas de Solana escritos en Rust puro (raw Rust) — sin el framework Anchor — probablemente almacenarán sus datos de una manera que no es directamente compatible con el método de serialización de Anchor que implementa AccountDeserialize::try_deserialize. Para deserializar datos que no son de Anchor, el desarrollador (dev) debe conocer de antemano el método de serialización utilizado — no existe una convención universal obligatoria en el ecosistema de Solana.
Ten precaución al leer datos de cuentas arbitrarias
Los programas de Solana son actualizables por defecto. La forma en que almacenan datos en sus cuentas podría cambiar en cualquier momento, lo que podría romper el programa que los está leyendo.
Aceptar datos de cuentas arbitrarias es peligroso — por lo general, se debe comprobar que la cuenta pertenece a un programa de confianza antes de leer sus datos.
Publicado originalmente el 7 de mayo de 2024