Hello! Welcome back to our journey into native Solana program development.
In our last lesson, we built the "front door" to our program: the process_instruction entrypoint. We learned that every instruction targeting our program arrives at this single function, which receives the instruction's details, including a raw byte array called instruction_data. We left off with the understanding that our program's first job is to figure out what this byte array means.
Today, we'll learn exactly how to do that. This lesson focuses on deserialization: the process of converting that opaque &[u8] byte slice into a structured Rust enum that our program can understand and act upon. We will use borsh, the standard serialization library in the Solana ecosystem, to accomplish this.
Your learning outcome is to deserialize instruction data within a program using the borsh library. By the end of this lesson, you'll be able to confidently transform a stream of bytes into meaningful commands for your on-chain logic.
Why We Need Deserialization
As a developer, you're already familiar with serialization, even if you don't always use the term. When your front-end application sends a JSON object to a backend API, it's serializing a JavaScript object into a string. The backend then deserializes that string back into an object or struct.
In Solana, the principle is the same, but the format is different. For efficiency and security, we don't use human-readable formats like JSON. Instead, we use a compact, binary format. The instruction_data you receive is this binary representation. Our task is to deserialize it.
To understand the solution borsh provides, it's helpful to first see the problem it solves. What does this byte array typically look like?
Solana Smart Contract Tutorial: Sending and Unpacking Instruction Data
This video from Josh's DevBox provides an excellent conceptual breakdown of how instruction data is structured in a byte array, before introducing any libraries.
Watch the segment "Understanding Instruction Data Format and Byte Representation" (07:51 - 12:36). Focus on these two key ideas: Instruction Tag: The first byte is used as a 'tag' or 'variant' to identify the instruction (e.g., 0 for Increment, 1 for Decrement, 2 for Set). Little-Endian: How multi-byte numbers like a u32 are ordered in memory, with the least significant byte first. This is a crucial concept in binary data representation.
As the video explains, a common convention is to structure the instruction_data with a leading byte that identifies the action, followed by the bytes that represent the arguments for that action.
You could, as shown later in that same video, write code to parse this byte array manually. You would read the first byte, use a match statement, and then carefully slice the rest of the array and convert bytes into numbers. This is tedious and highly error-prone. A small mistake in byte counting could lead to security vulnerabilities.
This is why we use borsh.
The borsh Solution
Borsh (Binary Object Representation Serializer for Hashing) is a serialization format designed specifically for security-critical projects like blockchains. It is:
- Deterministic: The same data structure always serializes to the exact same byte sequence.
- Compact: It produces a small binary output, saving precious transaction space and fees.
- Secure: It's designed to be unambiguous and safe to deserialize.
Instead of manual parsing, borsh allows us to simply declare how our data is structured and let the library handle the complex and error-prone work of byte manipulation.
Let's see how it works in practice.
This article, "Solana Program Instructions," demonstrates the standard borsh pattern for deserialization. It's a much cleaner approach than manual parsing.
Read the sections "Representing Instructions as a Rust Data Type" and "Deserializing Instruction Data". Pay close attention to: The enum definition for the instructions. The #[derive(BorshDeserialize)] attribute. This is the key piece of borsh magic. The use of try_from_slice() to perform the deserialization in one step.
The process described in the article is the standard you will use in native Solana development:
- Define Your Instructions: Create a Rust
enumwhere each variant represents a possible instruction your program can handle. Variants can contain data, likeUpdateNote { title: String, body: String, id: u64 }. - Derive the Trait: Add the
#[derive(BorshDeserialize, BorshSerialize)]attribute to yourenum(and any customstructs it uses). This macro automatically generates the serialization and deserialization code for you based on the structure of your data type. - Deserialize: In your
process_instructionfunction, call thetry_from_slice()method thatborshprovides on your instruction enum, passing it theinstruction_data.
Here's an example combining these steps:
use borsh::{BorshDeserialize, BorshSerialize};
use solana_program::{
account_info::AccountInfo,
entrypoint,
entrypoint::ProgramResult,
pubkey::Pubkey,
msg,
program_error::ProgramError,
};
// 1. Define the instruction enum
#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub enum MyInstruction {
Increment,
Decrement,
Set { value: u32 },
}
entrypoint!(process_instruction);
pub fn process_instruction(
_program_id: &Pubkey,
_accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
msg!("Entrypoint reached. Deserializing instruction...");
// 3. Deserialize the byte slice into our enum.
// The '?' operator will automatically return an error if deserialization fails.
let instruction = MyInstruction::try_from_slice(instruction_data)
.map_err(|err| {
msg!("Failed to deserialize instruction: {}", err);
ProgramError::InvalidInstructionData
})?;
// Now we can use a match statement on the structured data.
match instruction {
MyInstruction::Increment => msg!("Instruction: Increment"),
MyInstruction::Decrement => msg!("Instruction: Decrement"),
MyInstruction::Set { value } => msg!("Instruction: Set to {}", value),
}
Ok(())
}
Under the Hood of try_from_slice
So what is try_from_slice actually doing? It's performing the exact steps we identified earlier, but in a safe, automated way.
Demystifying Borsh Serialization in Solana Programs
The article 'Demystifying Borsh Serialization' provides a fantastic, step-by-step breakdown of what happens inside try_from_slice.
Read the sections titled "The Magic of Borsh Deserialization" and "Understanding Little-Endian". This will connect the raw byte layout we saw in the video to the automated process that borsh performs.
To summarize the magic, when you call MyInstruction::try_from_slice(&[2, 100, 0, 0, 0])?, borsh does the following:
- Reads the Variant: It reads the first byte,
2. - Checks the Enum: It looks at the definition of
MyInstruction. The variants are indexed starting from 0:Incrementis 0,Decrementis 1, andSetis 2. It finds a match. - Reads the Fields: It sees that the
Setvariant has one field of typeu32. It knows au32is 4 bytes long. - Parses the Payload: It reads the next 4 bytes:
[100, 0, 0, 0]. It interprets these as a little-endianu32, which evaluates to the number100. - Constructs the Object: It successfully constructs the Rust object:
MyInstruction::Set { value: 100 }.
If any step fails (e.g., the variant index is out of bounds, or there aren't enough bytes for the fields), try_from_slice returns an error, which your program can then safely handle.
Test your understanding!
You have the following instruction enum:
#[derive(BorshDeserialize, BorshSerialize, Debug)]
pub enum ThermostatInstruction {
TurnOff, // variant 0
SetTemp(u8), // variant 1
SetMode { is_heating: bool, is_cooling: bool }, // variant 2
}
A client sends an instruction with the following instruction_data byte array: [1, 22].
Which ThermostatInstruction variant will be created when you call ThermostatInstruction::try_from_slice(&[1, 22])? and why?
Note: In borsh, a u8 takes 1 byte, and a bool also takes 1 byte (0 for false, 1 for true).
Show answer
The result will be ThermostatInstruction::SetTemp(22).
Here's the breakdown:
- The first byte is
1.borshidentifies this as the index for the second variant,SetTemp. - It then sees that
SetTemprequires a payload of typeu8, which is 1 byte. - It reads the next byte,
22, and uses it as the value for theu8payload. - It successfully constructs
ThermostatInstruction::SetTemp(22).
The byte array for SetMode { is_heating: true, is_cooling: false } would look like [2, 1, 0].
Conclusion
You've now mastered a critical step in Solana program development. You've bridged the gap between the raw data sent by a client and the structured, type-safe world of your Rust code.
Here are the key takeaways from this lesson:
- Deserialization is the process of converting an instruction's raw byte data (
&[u8]) into a meaningful Ruststructorenum. borshis the standard, secure, and efficient library used for this in Solana.- The process is made simple by using the
#[derive(BorshDeserialize)]attribute on your data types. - The
try_from_slice()method performs the deserialization, automatically handling enum variants, field data, and byte ordering (little-endian). - Using
borshavoids the complexity and security risks of manual byte parsing.
In the previous lesson, you created the entrypoint. Now, you can process the data that comes through it. You're one step closer to implementing real business logic.
In our next lesson, we will shift our focus from the instruction_data to the other crucial input: the accounts array. You will learn how to read and interpret AccountInfo data passed to a program, which is where your program will store its state. Just like with instructions, borsh will be our tool for deserializing account data as well.
Can't find a good explanation? Sign up and we'll make it for you
Sign up