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,
};
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,
}
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);
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--
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");
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)?;
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);
}
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;
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,
}
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())?;
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;
Once we have successfully deserialized the data, we can now increment the counter.
greeting_account.serialize(&mut &mut account.data.borrow_mut()[..])?;
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(())
We then log the new counter and return the result of our program to signal that this transaction was successful.
Resources
- helloworld Rust program
- Solana programming model
- Solana YouTube channel
- Solana Program Library
Footnotes
Thank you for the guidance, support and review of this article.
@therealchaseeb, @jamesflorentino and @fastfrank.
Follow me on twitter to read more about developing on Solana.
Top comments (0)