Anchor vs Plain Rust
Anchor is recommended for beginners in all of the docs and guides.
The purpose of this guide is to elucidate how Rust development on Solana works without Anchor, so you can understand what's going on under the hood, as well as appreciate the conveniences Anchor provides.
This article goes over what's obscured from you when using Anchor, and what really happens underneath. We'll go over some docs examples of a simple counter program between raw Solana and Anchor.
Transactions and Instructions
The basic operating unit in Solana is an instruction
, which are held in transactions
which are their atomic, all or nothing counterpart.
Instructions hold three things:
- an array of accounts,
- the
program_id
that the transaction is targeting, - as well as
instruction_data
which is stored as an array ofu8
integers.
Why is instruction_data
a bunch of u8
?
Each of these numbers is one byte (8 bits) of the data stored in the instruction. These bytes will be decoded by your program to get the actual data. This data represents the actions the client wants to take with the program.
The client will create data structures (often Structs in Rust) and serialize them into this array of bytes, which the receiving program will then deserialize and make sense of.
Without Anchor, there's no real convention to this. It's up to the developer which serialization/deserialization strategy to use. This gives unlimited flexibility but makes it harder to read/write instructions when no convention is followed.
Let's look at the example Solana gives for their basic Rust program:
use solana_program::{
account_info::AccountInfo, entrypoint, entrypoint::ProgramResult, msg, pubkey::Pubkey,
};
entrypoint!(process_instruction);
pub fn process_instruction(
_program_id: &Pubkey,
_accounts: &[AccountInfo],
_instruction_data: &[u8],
) -> ProgramResult {
msg!("Hello, world!");
Ok(())
}
All Solana programs have a single entrypoint defined by the entrypoint!
macro.
Here's what a client to this program would look like in a test:
#[cfg(test)]
mod test {
use solana_program_test::*;
use solana_sdk::{
instruction::Instruction,
pubkey::Pubkey,
signature::Signer,
transaction::Transaction,
};
#[tokio::test]
async fn test_hello_world() {
let program_id = Pubkey::new_unique();
let mut program_test = ProgramTest::default();
program_test.add_program("hello_world", program_id, None);
let (mut banks_client, payer, recent_blockhash) = program_test.start().await;
// Create instruction
let instruction = Instruction {
program_id,
accounts: vec![],
data: vec![],
};
// Create transaction with instruction
let mut transaction = Transaction::new_with_payer(&[instruction], Some(&payer.pubkey()));
// Sign transaction
transaction.sign(&[&payer], recent_blockhash);
let transaction_result = banks_client.process_transaction(transaction).await;
assert!(transaction_result.is_ok());
}
}
Here we manually create the Instruction
and Transaction
, and run that transaction through the banks_client
which simulates the Solana runtime and runs it in-memory.
The banks client is a special type of client used only for testing. It's not as powerful as simulating with a real client but is useful for quickly simulating a certain state on the blockchain before your test runs.
For a deeper look at how a real program
would deserialize and parse the transaction data, see their example.
Instructions in anchor
Now let's take a look at similar Anchor examples and break down what's happening.
use anchor_lang::prelude::*;
declare_id!("6khKp4BeJpCjBY1Eh39ybiqbfRnrn2UzWeUARjQLXYRC");
#[program]
pub mod example {
use super::*;
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
let counter = &ctx.accounts.counter;
msg!("Counter account created! Current count: {}", counter.count);
Ok(())
}
pub fn increment(ctx: Context<Increment>) -> Result<()> {
let counter = &mut ctx.accounts.counter;
msg!("Previous counter: {}", counter.count);
counter.count += 1;
msg!("Counter incremented! Current count: {}", counter.count);
Ok(())
}
}
What is this Context
that seems to have super powers?
In Anchor, a lot of the lower down concepts like Transaction
and Instruction
get abstracted away. Instead we get a very declarative experience powered by macros, which do a ton of heavy lifting.
For example, what does a line like the one below do?
pub fn initialize(ctx: Context<Initialize>) -> Result<()>
We're telling Anchor that we want to use a set of account constraints via the Initialize
struct. We want it to auto-validate the constraints before running any logic.
Every Solana program is stateless, all state is passed in via accounts which act as buffers of data. They hold the state your program is acting on.
Context
wraps our structs and provides the additional metadata and checks for things like access control (can this person send this SOL?) and validations.
The Initialize
struct itself will #[derive(Accounts)]
which will implement important traits for handling serialization, deserialization and validations on the types of accounts we can pass the program
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub payer: Signer<'info>,
#[account(
init,
payer = payer,
space = 8 + 8
)]
pub counter: Account<'info, Counter>,
pub system_program: Program<'info, System>,
}
In this struct we're telling Anchor to apply constraints to these accounts.
- The
#[account(mut)]
line tells Anchor to ensure that thepayer
account MUST be mutable, because we're going to be extracting rent from it. - The
Signer<'info>
portion tells Anchor that this account must sign the transaction.
The #[account(init, payer = payer, space = 8 + 8)]
line tells Anchor a few things:
- First the
init
argument tells Anchor that it will be creating this account. - The next line says to use the
payer
account to fund it. Whereas thespace
argument tells Anchor to reserve 8 bytes for thediscriminator
(bytes that reflect the type of the following set of bytes), and 8 additional bytes for tracking the state of theu64
counter used in the example. - Finally, the line
pub system_program: Program<'info, System>
is necessary to callsystem_instruction::create_account
later in the code, as well as acting as a validator that the program is actually the correct program (by checking the ID).
So, Context
is a wrapper around the accounts that are required to run a given instruction, say for initialize
or increment
.
We're also telling it to automatically deserialize the account data (ex. Account<'info, Counter>
) so we can avoid manually translating from the bytes to a struct using borsh
, serde
or similar like we have to do in plain old Solana development.
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(mut)]
pub counter: Account<'info, Counter>,
}
Here we are telling anchor that the account we pass in the counter
slot is going to deserialize into a Counter
struct.
#[account]
pub struct Counter {
pub count: u64,
}
So to sum it up, you can think of the #derive(Accounts)
and #[account(...)]
macros as defining the structure and validation logic that Anchor uses to build the Context<T>
at runtime.
In essence they provide:
- Validators (is it mutable? does it sign?)
- Instruction directives (initialize this account, use this payer)
- Security checks (ownership, seeds, constraints, etc.)
By providing this information up-front on the program
, Anchor is able to construct IDLs, which provide a schema to work with as a client
.
Okay that's great that Context provides so much utility, but where are my Instructions?
This is where the IDL ties into everything, and is where we take a look at a client
in Anchor.
Inside the client
code we would find a line like this:
declare_program!(something);
This macro does a lot of heavy lifting, and will generate the client for us automatically.
Take for an example this snippet which initializes an account:
let initialize_ix = program
.request()
.accounts(accounts::Initialize {
counter: counter.pubkey(),
payer: program.payer(),
system_program: system_program::ID,
})
.args(args::Initialize)
.instructions()?
.remove(0);
It's the IDL that gives us the accounts::Initialize {}
struct.
So while we don't see any explicit/manual handling of Instruction
and Transaction
like we do in plain old Solana, Anchor instead is providing us the structs we need to use to interact with the program, and the parsing into Instructions and the greater Transaction is done automatically and without boilerplate.