Hello! Welcome to the fifth module of our Solana development course.
In the last module, we established a clear mental model of how a client application "talks" to a Solana program. We dissected the anatomy of an instruction, the core message sent to the blockchain, and identified its three key components: program_id, accounts, and data.
Today, we switch perspectives. We move from being the sender of the instruction to being the receiver. This is our first step into writing on-chain code. We will build the "front door" to a Solana program—the part that receives an instruction and begins to make sense of it.
Your learning outcome for this lesson is to write a basic Solana program entrypoint that processes an instruction. By the end of this session, you will understand the fundamental structure of a native Rust-based Solana program and how the Solana runtime hands off control to your code.
The Program's Front Door: The Entrypoint
Every executable program needs a starting point. For many applications you've built, this might be a main() function or an index.js file. In the world of native Solana programs, this starting point is called the entrypoint. It's a single, mandatory function that the Solana runtime calls whenever your program is invoked by an instruction.
Let's get a formal introduction to this concept.
The Solana documentation provides a concise overview of the minimal requirements for a Rust program, centered around the concept of an entrypoint.
Read the introductory paragraphs under the main title and the section titled "Program Structure". This will introduce the entrypoint requirement and a typical file layout you'll see in Solana projects.
As the documentation states, while you can structure your files however you like, the one non-negotiable piece is the entrypoint.
Defining the Entrypoint in Rust
In Rust, we use the solana_program crate, which provides the necessary tools and types for on-chain development. This crate includes a macro called entrypoint! that designates a specific function as the program's entrypoint.
By convention, this function is named process_instruction. Let's look at its signature and the code required to set it up.
Writing your Program's Entrypoint - Getting Started
This guide, "Writing your Program's Entrypoint", gives a focused look at the practical code for declaring the entrypoint and its processing function.
Read the entire short article. Pay close attention to: The use of the entrypoint! macro. The full function signature of process_instruction. The explanation of its role in decoding and dispatching instructions.
Let's break down the process_instruction function you just saw. This is the heart of our lesson today.
use solana_program::{
account_info::AccountInfo,
entrypoint,
entrypoint::ProgramResult,
pubkey::Pubkey,
msg, // A macro for logging messages to the chain
};
// Declares that process_instruction is the program's entrypoint
entrypoint!(process_instruction);
// The function that processes every instruction sent to the program
pub fn process_instruction(
program_id: &Pubkey, // The public key of our program
accounts: &[AccountInfo], // The accounts involved in the instruction
instruction_data: &[u8], // The instruction's data payload
) -> ProgramResult {
// Log a message to confirm the entrypoint was called
msg!("Hello, Solana! My program entrypoint was called.");
// For now, we just return Ok, indicating success.
Ok(())
}
Notice how the parameters directly correspond to the instruction components we studied in the previous lesson:
program_id: &Pubkey: This is the address of the currently executing program. It's useful for validating that certain accounts are owned by our program.accounts: &[AccountInfo]: This is the array of accounts that the client specified.AccountInfois a struct that gives you access to an account's data, lamport balance, owner, and other properties.instruction_data: &[u8]: This is the raw byte array sent by the client. This is the "how" of the instruction, containing the action to perform and its arguments.-> ProgramResult: The return type. This is an alias forResult<(), ProgramError>. Your function must either returnOk(())on success or an error of typeProgramError.
Your background as a front-end lead gives you a great analogy here: think of process_instruction as the single, main request handler for your backend API. Every call, regardless of the endpoint (/users, /posts, etc.), hits this one function first. The function's first job is to inspect the request (instruction_data) to figure out which specific logic to run.
From Entry to Action: Processing the Instruction
A program that only logs "Hello, Solana!" isn't very useful. The primary job of the process_instruction function is to act as a router. It needs to look at the instruction_data and decide which internal function to call.
As we touched on in the last lesson, a common pattern is to use the first byte(s) of instruction_data as a "tag" or "discriminator" to identify the intended action. The program then uses a match statement to route execution based on this tag.
Let's watch a demonstration of this pattern being implemented.
Solana Smart Contract Tutorial: Sending and Unpacking Instruction Data
In this clip from Josh's DevBox, you'll see how to evolve a basic process_instruction function into a router that handles multiple instruction types.
Watch the following two segments: Integrating Unpack (06:02 - 08:02): Observe how the instruction_data is passed to a dedicated unpack function. Don't worry about the details of unpacking yet—just focus on the idea that the raw data is being converted into a more structured form. Processing Instructions (19:22 - 20:44): This is the key part. See how a match statement is used on the unpacked instruction to execute different logic for Increment, Decrement, and Set.
The video demonstrates the complete flow:
- The
process_instructionentrypoint is called by the Solana runtime. - It passes the
instruction_datato a helper function (which we'll build in the next lesson) to deserialize it from raw bytes into a Rustenum. - It then uses a
matchstatement on that enum to execute the specific logic for the requested action.
Test your understanding!
You are writing a program to manage a simple "To-Do List". You've defined an instruction enum like this:
pub enum TodoInstruction {
InitializeList,
AddTask { task_content: String },
CompleteTask { task_index: u8 },
}
Inside your process_instruction function, after you've successfully unpacked the instruction_data into a variable instruction of type TodoInstruction, what would the match statement look like to route these instructions? Just sketch out the structure.
Show answer
The match statement would look something like this:
match instruction {
TodoInstruction::InitializeList => {
// Call a function to handle list initialization
// msg!("Initializing new to-do list...");
// process_initialize_list(accounts)?;
}
TodoInstruction::AddTask { task_content } => {
// Call a function to handle adding a task
// msg!("Adding new task: {}", task_content);
// process_add_task(accounts, task_content)?;
}
TodoInstruction::CompleteTask { task_index } => {
// Call a function to handle completing a task
// msg!("Completing task at index: {}", task_index);
// process_complete_task(accounts, task_index)?;
}
}
This structure cleanly separates the routing logic in process_instruction from the business logic in the individual process_... functions.
Conclusion
In this lesson, we've laid the cornerstone for our on-chain programs. We've moved from the client's perspective to the program's, writing the essential entrypoint that brings a Solana program to life.
Here are the key takeaways:
- Every native Solana program must have an entrypoint, declared in Rust using the
entrypoint!macro. - This macro points to a function, conventionally named
process_instruction, which the Solana runtime calls for every instruction targeting the program. - The
process_instructionfunction receives theprogram_id, an array ofaccounts, and the rawinstruction_dataas parameters. - Its main responsibility is to act as a router: it deserializes the
instruction_dataand uses amatchstatement to dispatch control to the appropriate business logic handler. - The function must return a
ProgramResult, which isOk(())on success or aProgramErroron failure.
You now have a complete, albeit simple, template for a native Solana program. You understand how the runtime invokes your code and how to begin processing the data it sends.
In our next lesson, we will tackle the next logical step: "Deserialize instruction data within a program using the borsh library." We'll take that opaque instruction_data: &[u8] byte slice and learn how to safely and efficiently turn it into the structured Rust enums and structs that our match statement can work with.
Can't find a good explanation? Sign up and we'll make it for you
Sign up