DEV Community

Cover image for Solana teardown: Walkthrough of the example helloworld program
Colin Ogoo
Colin Ogoo

Posted on

Solana teardown: Walkthrough of the example helloworld program

In this article, we'll walk through the helloworld Rust program line-by-line and begin to unpack how programs on Solana work.

Who this article is for

This walkthrough assumes that you've written code in any programming language or have a basic understanding of Rust. You don't need prior experience writing programs (smart contracts) on Solana or any other blockchain. I won't spend much time explaining the Rust specific code or idioms but rather focus on the Solana program.

Let's get started.

Use declaration

use borsh::{BorshDeserialize, BorshSerialize};
use solana_program::{
    account_info::{next_account_info, AccountInfo},
    entrypoint,
    entrypoint::ProgramResult,
    msg,
    program_error::ProgramError,
    pubkey::Pubkey,
};
Enter fullscreen mode Exit fullscreen mode

We bring libraries and traits into scope with the use declaration. It allows us to bind a full path to a new name for easier access.

Greeting Account

#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub struct GreetingAccount {
    /// number of greetings
    pub counter: u32,
}
Enter fullscreen mode Exit fullscreen mode

Programs are stateless. If you need to store state between transactions, you can do so using Accounts. In Solana, EVERYTHING is an account. The Solana blockchain only sees a list of accounts of varying sizes. If an account is marked "executable" in its metadata, then it is considered a program. We'll talk more on accounts later, but for now, the understanding we need is that; our Greeting Account can hold data, and this data is in the shape of our GreetingAccount struct. Our accounts data property will have a counter property which we'll use to store a count of all the greetings. This data will persist beyond the lifetime of our program.

Entrypoint

entrypoint!(process_instruction);
Enter fullscreen mode Exit fullscreen mode

We register the process_instruction as the entrypoint symbol which the Solana runtime looks up and calls when invoking the program.

process_instruction

pub fn process_instruction(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    _instruction_data: &[u8],
) -> ProgramResult {
// --snip--
Enter fullscreen mode Exit fullscreen mode

All programs have the same signature. Let's take an in-depth look at the parameters.

program_id

The program_id is the public key (the address) of the currently executing program. In our case, this will be the public key of our HelloWorld program.

accounts

Accounts really deserve their own post. For now, I'll keep it brief. The accounts is an ordered list of the needed accounts for this transaction. The only properties that an account owner can modify are lamports and data.

instruction_data

The _instruction_data is specific to the program and is usually used to specify what operations the program should perform. In this helloworld example, we only have one instruction, "greet", so the instruction_data is unused, hence the preceding _. That's a Rust idiom to keep the compiler happy.

Logging

msg!("Hello World Rust program entrypoint");
Enter fullscreen mode Exit fullscreen mode

We can output logs from our program using the msg! macro. The caution here is that these messages are visible by viewing the program logs on the solana explorer.

Accounts

let accounts_iter = &mut accounts.iter();
let account = next_account_info(accounts_iter)?;
Enter fullscreen mode Exit fullscreen mode

We take the list of accounts we received via our entrypoint and convert it into an iterable that we can iterate over. The Solana program library provides the next_account_info function that returns the next account in the list.

It's important to note that programming in Solana requires you to think about your "data model" or "program model", as it were. Since we have to specify all the accounts a transaction will need, we need to think about; what accounts our program needs, the purpose of the account, and the relationships between them. For example, when transferring lamports, an instruction may define the first account as the source and the second as the destination.

The instructions our program can perform are essentially the API of our program. Therefore, we must know how to call these APIs and what order we need to provide the accounts. We specify this in an instructions.rs file. An example of this can be found in the token program.

Validation and Security

if account.owner != program_id {
    msg!("Greeted account does not have the correct program id");
    return Err(ProgramError::IncorrectProgramId);
}
Enter fullscreen mode Exit fullscreen mode

It's always good practice to validate the accounts you receive and assert that they have the correct permissions and are the accounts you are expecting.

Since our program needs to modify the data property, we need to check that the program is the owner of the account we are about to change. If not, we error out of the program.

Have a look at programs written in the Solana Program Library. You'll always find an exhaustive list of checks validating the inputs to the program.

Serialization / Deserialization

let mut greeting_account = GreetingAccount::try_from_slice(&account.data.borrow())?;
greeting_account.counter += 1;
Enter fullscreen mode Exit fullscreen mode

Hidden in here are a lot of rust idioms, but we'll focus on the deserializing part.

In Solana, an account can store ANY binary data. The Solana blockchain doesn't know what that data is. It's the responsibility of the program or client to understand it. For you to store data, you incur a storage cost called rent. Rent is another pretty big topic, so we'll gloss over it for now and do an in-depth look when we do a teardown of the client app in a future post.

Back to the data. Each account can store its own type of data. In our case, the data we are storing is of type GreetingAccount. Since we store the data in binary, we need to deserialize it when reading it and serialize it when saving it.

Let's retake a look at our struct.

#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub struct GreetingAccount {
    /// number of greetings
    pub counter: u32,
}
Enter fullscreen mode Exit fullscreen mode

To increment the counter, we need to deserialize the binary data into a GreetingAccount. For this, we use the borsh library. Solana doesn't enforce how we serialize or deserialize our data, so the choice of library is up to us.

let mut greeting_account = GreetingAccount::try_from_slice(&account.data.borrow())?;
Enter fullscreen mode Exit fullscreen mode

We take the binary data in account.data and try to deserialize it into a GreetingAccount. If that fails, we'll get an error.

You can read more on the .borrow() syntax from the official Solana docs.

greeting_account.counter += 1;
Enter fullscreen mode Exit fullscreen mode

Once we have successfully deserialized the data, we can now increment the counter.

greeting_account.serialize(&mut &mut account.data.borrow_mut()[..])?;
Enter fullscreen mode Exit fullscreen mode

We call the serialize method on the greeting_acocunt, which serializes the GreetingAccount struct and updates the data property.

msg!("Greeted {} time(s)!", greeting_account.counter);

Ok(())
Enter fullscreen mode Exit fullscreen mode

We then log the new counter and return the result of our program to signal that this transaction was successful.

Resources

Footnotes

Thank you for the guidance, support and review of this article.

@therealchaseeb, @jamesflorentino and @fastfrank.

Discussion (0)