Solana programs
Accounts can be used to store code. We mark these accounts as executable and they become Programs on Solana.
These accounts are just like normal accounts but marked executable and have their owner assigned to a BPF loader.
Loaders are there to support different application binary interfaces. You don't need to worry about any others. In practice there is only really one, the upgradeable BPF loader.
Programs are stateless. No class variables or globals. All data is explicitly passed from the outside. They are pure in the sense that they, on their own, cannot persist, read or write any state.
Instead, you make a program read and write data by sending transactions. The programs provide endpoints that can be called via transactions, at least on Anchor.
Anchor is like Ruby on Rails, its a framework for smart contracts. The alternative to anchor is parsing bytecode to filter instructions via a discriminator.
Each program is handed 2 key pieces of data:
- The accounts that the program may read/write to/from during the transaction
- Additional arguments specific to the instruction
Program ID is just the address of the account the program is stored on.
To do any persistence or state they work with accounts. Programs are stored inside accounts but have their own address (a program ID).
Programs are written in rust with the solana-sdk
library. The latest loader has a built in upgrading mechanism, this is why its called the upgradeable BPF loader. But most people are writing contracts with Anchor because it makes things easier to understand and less prone to error.
Verifiable builds enable users to verify that on-chain programs match the publicly available source code.
Programs can be updated by an upgrade authority. A program becomes immutable when the upgrade authority is set to null.
Programs can be written in any programming language that compiles to SBF
(Solana Bytecode Format) which is a modification of eBPF
.
Calling convention
All Solana programs expose a single entrypoint that takes:
- The program ID
- Accounts array
- Instruction data (an u8 array)
If you call another program, you have to rely on them giving you client code or documentation to be able to determine how to properly form an instruction. Interfaces are opaque and ad-hoc.
So every program has a single entrypoint that looks like this:
use solana_program::{
account_info::AccountInfo,
entrypoint,
entrypoint::ProgramResult,
pubkey::Pubkey
}
entrypoint!(process_instruction)
fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8]
) -> ProgramResult {
// ...
}
Inside the program would switch based on the instruction data, and more sanely (if you wanted to) on a discriminator bit. Each branch would be a single instruction type.
Then users of your program will encode a set of instructions inside their transactions that can, in part, use your program to perform and persist changes.
Solana programs basically:
- Define an entrypoint function
- Decode the instruction type and call with the arguments
- Call the relevant instruction handler
- Do account and argument checks
- Process the instructions by changing state
Solana comes with built in programs which are divided into two sets:
- Native Programs
- Solana Program Library (SPL) programs
Native provides base functionality to operate validators such as the system program which is responsible for new accounts and transferring SOL.
SPL includes programs for creating, swapping and lending tokens etc.
Runtime
Every instruction has fixed compute budgets which cannot be exceeded.
There is no mechanic to pay for more compute. You have to make it fit. If you build a program that calls another, and that program becomes more expensive, your program will break, forever.
There is a 1232 byte
limit on messages which is very low. A public key takes 32 bytes, a signature takes 64. More recently Solana has given the ability to specify larger lists of accounts through a separate mechanism called an account lookup table.
For each program call the client needs to specify before which accounts the program will access and whether it wants to read or write. The runtime uses this information to schedule non-overlapping transactions to be executed in parallel while still guaranteeing data consistency.
Clients
Clients submit transactions and fetch data from RPC nodes using JSON RPC payloads sent to port 8899
. Sometimes there is a pubsub websocket at 8900
.
Programs have no read interfaces. Everything is write only. If you need to read you have to pull the raw data by account address via RPC and deserialize it yourself.
You should turn on preflight checks when testing client code. Turn them off when testing onchain programs. Transactions are sanity checked on the client before submission but errors and logging you get from the chain are way better. You control this through an options object you send functions.
For local testing you can turn commitment down to processed
so you don't have to wait for confirmation to see a result.
CLI
CLI tools are good in Solana. curlshell
and solana-install
version manager and you're set up.
solana-test-validator
and solana logs
are helpful.