En el tutorial anterior, aprendimos cómo leer cuentas que se pasan a un programa. Vimos que llamar a account.try_borrow_data() proporciona una referencia al campo de datos de la cuenta como un slice de bytes sin procesar, por ejemplo [0x01, 0x00, 0x00, 0x00].
Solana almacena todos los datos de las cuentas como bytes. Para trabajar con estructuras de datos de más alto nivel como los structs de Rust, utilizamos la serialización para convertir los structs en bytes y almacenarlos on-chain, y la deserialización para convertir esos bytes nuevamente en structs al leerlos. Solana utiliza Borsh como su formato de serialización estándar.
Este artículo explica cómo funciona la serialización Borsh y cómo interpretar estos bytes sin procesar.
En este tutorial, mostraremos lo siguiente:
- Qué es la serialización y cómo funciona la serialización Borsh en Solana
- Cómo leer e interpretar los datos serializados de una cuenta
- Cómo se ve una cuenta sin datos cuando intentas leerla
¿Qué es la Serialización y la Deserialización?
La serialización es el proceso de convertir estructuras de datos (como un struct de Rust o un string) en una secuencia de bytes que puede almacenarse o transmitirse. La deserialización es el proceso inverso: convertir esos bytes de vuelta a la estructura de datos original.
Por ejemplo, si tienes un struct con un valor de contador de 42 (almacenado como un u64), la serialización lo convierte en 8 bytes: [0x2A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]. El primer byte 0x2A contiene el byte menos significativo del valor en formato little-endian, y los 7 bytes restantes son ceros porque u64 se almacena como 8 bytes en memoria. Más adelante, cuando necesites leer esos datos, la deserialización convierte esos bytes nuevamente en tu struct.
¿Qué es la Serialización Borsh?
Borsh (Binary Object Representation Serializer for Hashing) es un formato de serialización que define las reglas para convertir structs de Rust en bytes y viceversa. Solana usa Borsh como su formato de serialización estándar porque es:
- Determinista: Los mismos datos siempre producen los mismos bytes (y los mismos bytes siempre producen los mismos datos)
- Compacto: Almacena datos de manera eficiente sin metadatos adicionales ni relleno (padding) entre campos (los tipos de tamaño fijo usan su tamaño en bytes estándar, los tipos de longitud variable usan solo lo que necesitan más un prefijo de longitud de 4 bytes)
El crate de Rust borsh implementa el formato de serialización que utilizan los programas de Solana.
Los programas de Solana usan Borsh para serializar tanto los datos de instrucción como los datos de cuentas on-chain. Para las cuentas, Borsh convierte los structs de Rust y sus campos (strings, enteros, booleanos, vectores, etc.) en bytes sin procesar que se almacenan en el campo de datos de la cuenta.
Cómo Funciona la Serialización Borsh
En los programas nativos de Rust, definimos structs para representar los datos de la cuenta que queremos almacenar (similar a cómo defines structs de cuentas en Anchor). Borsh luego serializa estos structs en bytes que llenan el campo data de la cuenta. Cuando se serializan, los campos del struct se disponen de forma contigua en el campo de datos de la cuenta, en el mismo orden en que se declaran.
El flujo básico se muestra en el diagrama a continuación:

Este diagrama muestra:
- Serialización: Tu struct de Rust (por ejemplo,
CounterData { count: 42 }) se convierte en bytes sin procesar utilizando Borsh - Almacenamiento: Esos bytes se almacenan en el campo de datos de la cuenta on-chain
- Deserialización: Al leer la cuenta, Borsh convierte esos bytes nuevamente en tu struct
¿Cómo se Ven los Datos de una Cuenta de Solana Después de la Serialización Borsh?
En Anchor, la serialización está abstraída, pero en los programas nativos de Rust agregamos el atributo #[derive(BorshSerialize, BorshDeserialize)] a nuestros structs. Esto le indica a la biblioteca Borsh que genere automáticamente el código de serialización y deserialización en tiempo de compilación.
Por ejemplo, aquí hay un struct CounterData que almacena un valor de contador como u64:
#[derive(BorshSerialize, BorshDeserialize)]
pub struct CounterData {
pub count: u64,
}
Si el valor de count es 42, el atributo BorshSerialize serializará este struct a:

Esto es lo que ocurre:
- Borsh toma nuestro valor de cuenta (42) y lo convierte a hexadecimal: 42 en decimal =
0x2Aen hex - Dado que el campo de cuenta está definido como un
u64, Borsh usa 8 bytes en formato little-endian para representarlo - Los 7 bytes restantes después de
0x2Ason ceros porque unu64ocupa 8 bytes
Cómo Borsh Serializa Datos de Longitud Variable
Antes de ver ejemplos más complejos, comprendamos cómo Borsh maneja los tipos de datos que no tienen longitudes fijas, como los strings y los vectores.
A diferencia de los tipos de datos fijos como u64, bool y u8, que utilizan el mismo número de bytes independientemente de su valor, los tipos de datos de longitud variable como String y Vec<T> tienen tamaños diferentes según su contenido.
Para los tipos de longitud variable, Borsh utiliza prefijos de longitud (length prefixing): primero escribe el tamaño de los datos y luego los datos en sí. Esto le indica a Borsh cuántos bytes debe leer al deserializar (sin ello, Borsh no sabría dónde termina un campo y comienza otro).
Así es como funciona:
- Primero, Borsh serializa la longitud de los datos como un
u32(4 bytes) en formato little-endian - Luego, serializa los bytes reales de los datos
Por ejemplo, si tenemos un string “hi”:
- Primero, Borsh serializa la longitud como un
u32en little-endian:[0x02, 0x00, 0x00, 0x00](2 bytes). Debido a que la longitud se almacena como unu32, el tamaño máximo del string es teóricamente2^32 - 1bytes - Finalmente, Borsh serializa los bytes reales UTF-8 para “hi”:
[0x68, 0x69]
Esto nos da el resultado final:

Este prefijo de longitud ayuda a Borsh a saber exactamente cuántos bytes leer para un tipo de longitud variable durante la deserialización.
¿Cómo Serializa Borsh un Struct de Cuenta de Solana con Múltiples Campos?
Supongamos que tenemos un struct UserData con múltiples campos para representar la información del usuario en una cuenta de almacenamiento:
#[derive(BorshSerialize, BorshDeserialize)]
struct UserData {
active: bool, // 1 byte: 0x01 for true, 0x00 for false
age: u8, // 1 byte
name: String, // 4 bytes (length) + UTF-8 bytes
scores: Vec<u8>, // 4 bytes (length) + individual u8 values
}
let user = UserData {
active: true,
age: 25,
name: "hi".to_string(),
scores: vec![95, 87, 92],
};
Borsh serializa los campos del struct estrictamente en el orden en que están definidos en el struct, independientemente de si son de tamaño fijo o de longitud variable (tamaño dinámico). La ubicación de los campos de tamaño fijo y de longitud variable no afecta cómo Borsh los maneja: cada campo se serializa de acuerdo con las reglas de su tipo (los tipos de tamaño fijo usan su tamaño estándar de bytes, los tipos de longitud variable usan el prefijo de longitud), y todos los campos se disponen secuencialmente en el orden de declaración. Esto es lo que ocurre:
- Primero, Borsh serializa el campo
active(true) a 1 byte:0x01. - Luego serializa
age(25) a 1 byte:0x19. - A continuación, serializa el campo
name(“hi”). Dado que los strings tienen un tamaño dinámico, Borsh utiliza un enfoque de prefijo de longitud:- Primero escribe la longitud como un
u32en little-endian:[0x02, 0x00, 0x00, 0x00](2 bytes) - Luego escribe los bytes UTF-8 reales para “hi” (
[0x68, 0x69])
- Primero escribe la longitud como un
- Finalmente, serializa el vector
scores[95, 87, 92]utilizando el mismo enfoque de prefijo de longitud:- Escribe la longitud del vector como
u32en little-endian (3 elementos =[0x03, 0x00, 0x00, 0x00]) - Luego escribe cada valor
u8:[0x5F, 0x57, 0x5C].
- Escribe la longitud del vector como
Todos estos bytes se adjuntan juntos en el orden en que se declaran los campos, dándonos el resultado final:

Esto muestra cómo Borsh maneja cuentas con múltiples campos, incluyendo String y Vec.
¿Cómo Leemos de Vuelta los Datos de la Cuenta Serializados?
Para recuperar nuestros datos de una cuenta de Solana, los deserializamos. La biblioteca Borsh proporciona una función try_from_slice que maneja la deserialización leyendo los bytes en el orden en que fueron serializados y reconstruyendo el struct original.
Por lo tanto, para una cuenta de Solana pasada a un programa nativo, el flujo es:
- Leer los bytes sin procesar de la cuenta
- Llamar a
try_from_slicedesde el crate Borsh para deserializar esos bytes en el struct original
El siguiente código muestra esto en la práctica. La función read_user_account a continuación es una representación conceptual que demuestra cómo usar try_from_slice para la deserialización. El parámetro account representa una cuenta general de Solana que contiene el struct UserData de la sección anterior “¿Cómo Serializa Borsh un Struct de Cuenta de Solana con Múltiples Campos?” (con los campos active, age, name y scores).
use borsh::BorshDeserialize;
use solana_program::account_info::AccountInfo;
pub fn read_user_account(account: &AccountInfo) -> ProgramResult {
// First, we get the raw bytes from the account
let data = account.try_borrow_data()?;
// The raw bytes in the account's data field:
// [0x01, 0x19, 0x02, 0x00, 0x00, 0x00, 0x68, 0x69, 0x03, 0x00, 0x00, 0x00, 0x5F, 0x57, 0x5C]
// Then we use Borsh to deserialize these bytes back into our struct
let _user = UserData::try_from_slice(&data)?;
// We get back our original data:
// UserData { active: true, age: 25, name: "hi", scores: [95, 87, 92] }
Ok(())
}
Reglas de Serialización Borsh para Otros Tipos Comunes
Para otros tipos de campos comunes usados en cuentas de Solana como booleanos, números (u32, i32, u64) y Pubkeys, Borsh sigue estas reglas:
| Tipo | Tamaño | Formato | Ejemplo |
|---|---|---|---|
bool |
1 byte | 0x01 para true, 0x00 para false |
true → [0x01] |
u8 |
1 byte | Valor sin procesar | 42 → [0x2A] |
u16 |
2 bytes | Little-endian | 42 → [0x2A, 0x00] |
u32 |
4 bytes | Little-endian | 42 → [0x2A, 0x00, 0x00, 0x00] |
u64 |
8 bytes | Little-endian | 42 → [0x2A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00] |
u128 |
16 bytes | Little-endian | Patrón similar con 16 bytes |
i8, i16, i32, i64, i128 |
Igual que sin signo | Little-endian, complemento a dos | -1 como i8 → [0xFF], los valores positivos se serializan igual que los sin signo |
Pubkey |
32 bytes | Bytes sin procesar | Los 32 bytes de la clave pública |
El mismo principio de adjuntar que vimos anteriormente se aplica aquí también. Cuando tienes un struct con estos tipos, Borsh recorre cada campo en orden y adjunta todos los bytes juntos, sin padding ni metadatos adicionales (esto mantiene los datos serializados lo más pequeños posible).
Leer Bytes Sin Procesar Sin Deserialización
Puedes leer manualmente campos específicos de los datos de la cuenta sin deserializar si conoces el diseño de la memoria. Esto no se recomienda para código de producción (es propenso a errores), pero ayuda a entender cómo funciona Borsh.
Supongamos que tenemos nuestra cuenta CounterData anterior con solo el campo count: 42, serializado como [0x2A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]. Podemos leer solo el valor de count de forma manual extrayendo los primeros 8 bytes y convirtiéndolos en un u64:
pub fn read_count_manually(account: &AccountInfo) -> ProgramResult {
let data = account.try_borrow_data()?;
// count is a u64, so it occupies the first 8 bytes
if data.len() >= 8 {
let count_bytes = &data[0..8];
let count = u64::from_le_bytes(count_bytes.try_into().unwrap());
msg!("Count value: {}", count); // This will print: Count value: 42
}
Ok(())
}
Nota: Dado que count es el único campo en CounterData, intentar leer más allá de su longitud de 8 bytes causaría un panic, ya que no hay datos adicionales en el struct.
Ejecutar el código anterior registrará un valor de count de 42.

Esto funciona porque conocemos el diseño del struct. El campo count ocupa los primeros 8 bytes (posiciones de byte 0–7), por lo que podemos leerlos directamente y convertirlos de nuevo a un u64. En un programa real, usaríamos CounterData::try_from_slice(&data)? en su lugar, el cual deserializa automáticamente todo el struct a partir de los bytes sin procesar.
Acceso a los Metadatos de la Cuenta (Lamports, Owner, etc.)
Hasta ahora hemos estado hablando sobre el campo de datos de la cuenta, pero como aprendimos en tutoriales anteriores, las cuentas de Solana tienen otros campos importantes como lamports, owner, pubkey, etc.
account.try_borrow_data()? solo nos da el campo de datos (donde residen nuestros structs serializados por Borsh), pero el struct AccountInfo pasado a nuestra función process_instruction nos da acceso a todos los demás metadatos de la cuenta que Solana mantiene de manera automática.
Mostramos cómo acceder a estos campos en el código a continuación:
pub fn process_instruction(
_program_id: &Pubkey,
accounts: &[AccountInfo],
_instruction_data: &[u8],
) -> ProgramResult {
// Get the account's lamports (balance)
let lamports = account.lamports();
msg!("Account lamports: {}", lamports);
// Get the account's owner (program that controls it)
let owner = account.owner;
msg!("Account owner: {}", owner);
// Get the account's public key
let pubkey = account.key;
msg!("Account pubkey: {}", pubkey);
// Check if the account is a signer
let is_signer = account.is_signer;
msg!("Is signer: {}", is_signer);
// Check if the account is writable
let is_writable = account.is_writable;
msg!("Is writable: {}", is_writable);
Ok(())
}
Cuando ejecutamos el código anterior, obtenemos algo como esto:

AccountInfo es el tipo de struct que representa una cuenta de Solana en tu programa. Contiene todos los metadatos de la cuenta (lamports, owner, etc.) y proporciona métodos para acceder al campo de datos. Cuando llamamos a account.try_borrow_data()?, estamos accediendo solo al campo de datos donde se almacenan nuestros structs serializados por Borsh.
Resumen
En este tutorial, cubrimos cómo funciona la serialización Borsh en Solana:
- La serialización convierte los structs de Rust en bytes para su almacenamiento, mientras que la deserialización convierte los bytes nuevamente en structs
- Borsh es el formato de serialización estándar de Solana: es determinista y compacto
- El atributo
#[derive(BorshSerialize, BorshDeserialize)]habilita la serialización Borsh para tus structs - Borsh serializa el campo de datos de las cuentas de Solana, no los otros campos de metadatos
- Los tipos de longitud fija (como
u64,bool) usan un número consistente de bytes - Los tipos de longitud variable (como
String,Vec<T>) usan prefijos de longitud: 4 bytes para la longitud + los datos reales - Los campos del struct se serializan secuencialmente en el orden en que se declaran, sin padding
AccountInfoproporciona acceso a todos los metadatos de la cuenta, mientras queaccount.try_borrow_data()?nos da solo el campo de datos serializado
En el siguiente tutorial, pondremos en práctica este conocimiento creando cuentas de almacenamiento y leyendo sus datos en programas nativos de Solana en Rust.
Este artículo es parte de una serie de tutoriales sobre el desarrollo en Solana.